1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-09-02 13:52:46 +02:00

feat: roadmap chat (#8666)

* wip: roadmap chat

* wip

* wip

* wip

* wip

* wip: message rendering

* wip: roadmap topics rendering

* Update UI for premium page

* Update UI for premium page

* wip: topic resources rendering

* Update premium page design

* Update navigation design

* Update design for play iconsg

* Update design for play iconsg

* wip: resource progress

* Update premium page

* Refactor premium page

* wip: resource progress

* feat: enhance the progress rendering

* feat: add abort functionality to chat and update UI elements

* feat: add roadmap recommendations feature and new query for built-in roadmaps

* Grid -> flex

* Update chat

* Handle error screen

* wip

* fix: empty content check

* feat: show loading status

* feat: ai chat limit

* Refactor content parsing

* Handle the wrapping of tag in codeblocks

* Refactor

* Refactor chat

* Fix hydration error

* feat: topic details

* style: adjust padding and icon size in RoadmapAIChat component

* feat: add tab navigation for chat and topic details

* fix: chat and details scrolling issue

* fix: invalidate progress query

* Fix upgrade modal and issue with close button

* Refactor topic detail AI

* Update progress UI

* Shorten the text

* Rewrite frontend roadmap content

* Minor update to content

* Update shrinking issue

* Changes to ai chat

* UI changes for chat card

* UI changes for chat card

* Refactor progress actions list

* Update progress marking UI

* UI Changes

* Responsiveness of sidebar

* Responsiveness of sidebar

* Mobile window for roadmap chat

* Responsiveness of AI chat

* Close button fix

* Make roadmap ai chat header responsive

* Escape key handling to close

* Update topic rendering node

* Make default active tab to AI in topic popup

* User progress component

* Clear chat only visible when there is chat

* wip: user persona

* fix: hide upgrade button

* fix: show chat input for guest users

* feat: persona update

* Roadmap recommendations

* Update persona form

* Update tell us more

* Fix persona form

* Fix scroll issue

* Lock body scroll when mobile is active

* Add intro message

* Refactor and update

* Update UI for AI chat

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
This commit is contained in:
Arik Chakma
2025-05-28 02:02:36 +06:00
committed by GitHub
parent 6edf2869f8
commit 62f31a4964
175 changed files with 4645 additions and 257 deletions

View File

@@ -3,6 +3,6 @@
"enabled": false
},
"_variables": {
"lastUpdateCheck": 1747060270496
"lastUpdateCheck": 1748277554631
}
}

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

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

View File

@@ -42,6 +42,14 @@
"@roadmapsh/editor": "workspace:*",
"@tailwindcss/vite": "^4.1.7",
"@tanstack/react-query": "^5.76.1",
"@tiptap/core": "^2.12.0",
"@tiptap/extension-document": "^2.12.0",
"@tiptap/extension-paragraph": "^2.12.0",
"@tiptap/extension-placeholder": "^2.12.0",
"@tiptap/extension-text": "^2.12.0",
"@tiptap/pm": "^2.12.0",
"@tiptap/react": "^2.12.0",
"@tiptap/suggestion": "^2.12.0",
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5",
"astro": "^5.7.13",
@@ -80,6 +88,7 @@
"shiki": "^3.4.2",
"slugify": "^1.6.6",
"tailwind-merge": "^3.3.0",
"tippy.js": "^6.3.7",
"tailwindcss": "^4.1.7",
"tiptap-markdown": "^0.8.10",
"turndown": "^7.2.0",

138
pnpm-lock.yaml generated
View File

@@ -41,6 +41,30 @@ importers:
'@tanstack/react-query':
specifier: ^5.76.1
version: 5.76.1(react@19.1.0)
'@tiptap/core':
specifier: ^2.12.0
version: 2.12.0(@tiptap/pm@2.12.0)
'@tiptap/extension-document':
specifier: ^2.12.0
version: 2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))
'@tiptap/extension-paragraph':
specifier: ^2.12.0
version: 2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))
'@tiptap/extension-placeholder':
specifier: ^2.12.0
version: 2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
'@tiptap/extension-text':
specifier: ^2.12.0
version: 2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))
'@tiptap/pm':
specifier: ^2.12.0
version: 2.12.0
'@tiptap/react':
specifier: ^2.12.0
version: 2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@tiptap/suggestion':
specifier: ^2.12.0
version: 2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
'@types/react':
specifier: ^19.1.4
version: 19.1.4
@@ -158,6 +182,9 @@ importers:
tailwindcss:
specifier: ^4.1.7
version: 4.1.7
tippy.js:
specifier: ^6.3.7
version: 6.3.7
tiptap-markdown:
specifier: ^0.8.10
version: 0.8.10(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))
@@ -983,6 +1010,9 @@ packages:
engines: {node: '>=18'}
hasBin: true
'@popperjs/core@2.11.8':
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
'@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
@@ -1396,9 +1426,56 @@ packages:
peerDependencies:
'@tiptap/pm': ^2.7.0
'@tiptap/extension-bubble-menu@2.12.0':
resolution: {integrity: sha512-DYijoE0igV0Oi+ZppFsp2UrQsM/4HZtmmpD78BJM9zfCbd1YvAUIxmzmXr8uqU18OHd1uQy+/zvuNoUNYyw67g==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-document@2.12.0':
resolution: {integrity: sha512-sA1Q+mxDIv0Y3qQTBkYGwknNbDcGFiJ/fyAFholXpqbrcRx3GavwR/o0chBdsJZlFht0x7AWGwUYWvIo7wYilA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-floating-menu@2.12.0':
resolution: {integrity: sha512-BYpyZx/56KCDksWuJJbhki/uNgt9sACuSSZFH5AN1yS1ISD+EzIxqf6Pzzv8QCoNJ+KcRNVaZsOlOFaJGoyzag==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-paragraph@2.12.0':
resolution: {integrity: sha512-QNK5cgewCunWFxpLlbvvoO1rrLgEtNKxiY79fctP9toV+e59R+1i1Q9lXC1O5mOfDgVxCb6uFDMsqmKhFjpPog==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-placeholder@2.12.0':
resolution: {integrity: sha512-K7irDox4P+NLAMjVrJeG72f0sulsCRYpx1Cy4gxKCdi1LTivj5VkXa6MXmi42KTCwBu3pWajBctYIOAES1FTAA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-text@2.12.0':
resolution: {integrity: sha512-0ytN9V1tZYTXdiYDQg4FB2SQ56JAJC9r/65snefb9ztl+gZzDrIvih7CflHs1ic9PgyjexfMLeH+VzuMccNyZw==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm@2.12.0':
resolution: {integrity: sha512-TNzVwpeNzFfHAcYTOKqX9iU4fRxliyoZrCnERR+RRzeg7gWrXrCLubQt1WEx0sojMAfznshSL3M5HGsYjEbYwA==}
'@tiptap/react@2.12.0':
resolution: {integrity: sha512-D+PR+4kJO9h8AB/7XyQ/Anw8tqeS2ecv5QemBOCHi9JlMAjytauUrj6IfFBO9RbsCowlBjW5GnSpFhzpk2Gghg==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
'@tiptap/suggestion@2.12.0':
resolution: {integrity: sha512-bsXLoZbjUo1oOF1Z+XSfoGzbcnrTcYtJdfylM/FerMLU9T12dhsM/Ri2SKLX4IR5D0HJ07FcsEHCrGEy8Y5y0A==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tybys/wasm-util@0.9.0':
resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==}
@@ -1527,6 +1604,9 @@ packages:
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
'@types/use-sync-external-store@0.0.6':
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
@@ -2859,6 +2939,7 @@ packages:
node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
deprecated: Use your platform's native DOMException instead
node-fetch-native@1.6.6:
resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==}
@@ -3546,6 +3627,9 @@ packages:
resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==}
engines: {node: '>=12.0.0'}
tippy.js@6.3.7:
resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==}
tiptap-markdown@0.8.10:
resolution: {integrity: sha512-iDVkR2BjAqkTDtFX0h94yVvE2AihCXlF0Q7RIXSJPRSR5I0PA1TMuAg6FHFpmqTn4tPxJ0by0CK7PUMlnFLGEQ==}
peerDependencies:
@@ -4606,6 +4690,8 @@ snapshots:
dependencies:
playwright: 1.52.0
'@popperjs/core@2.11.8': {}
'@remirror/core-constants@3.0.0': {}
'@resvg/resvg-js-android-arm-eabi@2.6.2':
@@ -4925,6 +5011,35 @@ snapshots:
dependencies:
'@tiptap/pm': 2.12.0
'@tiptap/extension-bubble-menu@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
'@tiptap/pm': 2.12.0
tippy.js: 6.3.7
'@tiptap/extension-document@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
'@tiptap/extension-floating-menu@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
'@tiptap/pm': 2.12.0
tippy.js: 6.3.7
'@tiptap/extension-paragraph@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
'@tiptap/extension-placeholder@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
'@tiptap/pm': 2.12.0
'@tiptap/extension-text@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
'@tiptap/pm@2.12.0':
dependencies:
prosemirror-changeset: 2.3.0
@@ -4946,6 +5061,23 @@ snapshots:
prosemirror-transform: 1.10.4
prosemirror-view: 1.39.2
'@tiptap/react@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
'@tiptap/extension-bubble-menu': 2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
'@tiptap/extension-floating-menu': 2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
'@tiptap/pm': 2.12.0
'@types/use-sync-external-store': 0.0.6
fast-deep-equal: 3.1.3
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
use-sync-external-store: 1.5.0(react@19.1.0)
'@tiptap/suggestion@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
'@tiptap/pm': 2.12.0
'@tybys/wasm-util@0.9.0':
dependencies:
tslib: 2.8.1
@@ -5092,6 +5224,8 @@ snapshots:
'@types/unist@3.0.3': {}
'@types/use-sync-external-store@0.0.6': {}
'@ungap/structured-clone@1.3.0': {}
'@vitejs/plugin-react@4.4.1(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4))':
@@ -7409,6 +7543,10 @@ snapshots:
fdir: 6.4.4(picomatch@4.0.2)
picomatch: 4.0.2
tippy.js@6.3.7:
dependencies:
'@popperjs/core': 2.11.8
tiptap-markdown@0.8.10(@tiptap/core@2.12.0(@tiptap/pm@2.12.0)):
dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)

BIN
public/images/gifs/bot.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 KiB

BIN
public/images/gifs/wave.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

View File

@@ -2,20 +2,22 @@ import { Menu } from 'lucide-react';
import { useState } from 'react';
import { AITutorSidebar, type AITutorTab } from './AITutorSidebar';
import { RoadmapLogoIcon } from '../ReactIcons/RoadmapLogo';
import { cn } from '../../lib/classname';
type AITutorLayoutProps = {
children: React.ReactNode;
activeTab: AITutorTab;
wrapperClassName?: string;
};
export function AITutorLayout(props: AITutorLayoutProps) {
const { children, activeTab } = props;
const { children, activeTab, wrapperClassName } = props;
const [isSidebarFloating, setIsSidebarFloating] = useState(false);
return (
<>
<div className="flex flex-row items-center justify-between border-b border-slate-200 px-4 py-3 lg:hidden sticky top-0 bg-white z-10">
<div className="sticky top-0 z-10 flex flex-row items-center justify-between border-b border-slate-200 bg-white px-4 py-3 lg:hidden">
<a href="/" className="flex flex-row items-center gap-1.5">
<RoadmapLogoIcon className="size-6 text-gray-500" color="black" />
</a>
@@ -27,13 +29,18 @@ export function AITutorLayout(props: AITutorLayoutProps) {
</button>
</div>
<div className="flex flex-grow lg:h-screen flex-row">
<div className="flex flex-grow flex-row lg:h-screen">
<AITutorSidebar
onClose={() => setIsSidebarFloating(false)}
isFloating={isSidebarFloating}
activeTab={activeTab}
/>
<div className="flex flex-grow flex-col overflow-y-scroll bg-gray-100 p-3 lg:px-4 lg:py-4">
<div
className={cn(
'flex flex-grow flex-col overflow-y-scroll bg-gray-100 p-3 lg:p-4',
wrapperClassName,
)}
>
{children}
</div>
</div>

View File

@@ -1,9 +1,15 @@
import {
BookOpen, Compass,
Plus,
Star,
X,
Zap
} from 'lucide-react';
import { useEffect, useState } from 'react';
import { BookOpen, Compass, Plus, Star, X, Zap } from 'lucide-react';
import { AITutorLogo } from '../ReactIcons/AITutorLogo';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { useIsPaidUser } from '../../queries/billing';
import { isLoggedIn } from '../../lib/jwt';
import { useIsPaidUser } from '../../queries/billing';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { AITutorLogo } from '../ReactIcons/AITutorLogo';
type AITutorSidebarProps = {
isFloating: boolean;
@@ -24,6 +30,12 @@ const sidebarItems = [
href: '/ai/courses',
icon: BookOpen,
},
// {
// key: 'chat',
// label: 'AI Chat',
// href: '/ai/chat',
// icon: Bot,
// },
{
key: 'staff-picks',
label: 'Staff Picks',

View File

@@ -2,8 +2,9 @@ import type { FormEvent } from 'react';
import { useId, useState } from 'react';
import { httpPost } from '../../lib/http';
import {
COURSE_PURCHASE_PARAM, FIRST_LOGIN_PARAM,
setAuthToken
COURSE_PURCHASE_PARAM,
FIRST_LOGIN_PARAM,
setAuthToken,
} from '../../lib/jwt';
type EmailLoginFormProps = {
@@ -65,7 +66,11 @@ export function EmailLoginForm(props: EmailLoginFormProps) {
const passwordFieldId = `form:${useId()}`;
return (
<form className="w-full" onSubmit={handleFormSubmit}>
<form
className="w-full"
onSubmit={handleFormSubmit}
suppressHydrationWarning={true} // Hubspot adds data-* attributes which causes hydration errors
>
<label htmlFor={emailFieldId} className="sr-only">
Email address
</label>

View File

@@ -6,7 +6,7 @@ import { AuthenticationForm } from './AuthenticationForm';
<Popup id='login-popup' title='' subtitle=''>
<div class='mb-7 text-center'>
<p class='mb-3 text-2xl font-semibold leading-5 text-slate-900'>
<p class='mb-3 text-2xl leading-5 font-semibold text-slate-900'>
Login or Signup
</p>
<p class='mt-2 text-sm leading-4 text-slate-600'>

View File

@@ -0,0 +1,25 @@
.chat-editor .tiptap p.is-editor-empty:first-child::before {
color: #adb5bd;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
.chat-editor .tiptap p:first-child.is-empty::before {
color: #adb5bd;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
.chat-editor .tiptap [data-type='variable'] {
font-size: 12px;
font-weight: 500;
line-height: 1.5;
padding: 2px 4px;
border-radius: 8px;
background-color: #f0f5ff;
color: #2c5df1;
}

View File

@@ -0,0 +1,122 @@
import './ChatEditor.css';
import {
Editor,
EditorContent,
useEditor,
type JSONContent,
} from '@tiptap/react';
import DocumentExtension from '@tiptap/extension-document';
import ParagraphExtension from '@tiptap/extension-paragraph';
import TextExtension from '@tiptap/extension-text';
import Placeholder from '@tiptap/extension-placeholder';
import { VariableExtension } from './VariableExtension/VariableExtension';
import { variableSuggestion } from './VariableExtension/VariableSuggestion';
import { queryClient } from '../../stores/query-client';
import { roadmapTreeMappingOptions } from '../../queries/roadmap-tree';
import { useQuery } from '@tanstack/react-query';
import { useEffect, type RefObject } from 'react';
import { roadmapDetailsOptions } from '../../queries/roadmap';
const extensions = [
DocumentExtension,
ParagraphExtension,
TextExtension,
Placeholder.configure({
placeholder: 'Ask AI anything about the roadmap...',
}),
VariableExtension.configure({
suggestion: variableSuggestion(),
}),
];
const content = '<p></p>';
type ChatEditorProps = {
editorRef: RefObject<Editor | null>;
roadmapId: string;
onSubmit: (content: JSONContent) => void;
};
export function ChatEditor(props: ChatEditorProps) {
const { roadmapId, onSubmit, editorRef } = props;
const { data: roadmapTreeData } = useQuery(
roadmapTreeMappingOptions(roadmapId),
queryClient,
);
const { data: roadmapDetailsData } = useQuery(
roadmapDetailsOptions(roadmapId),
queryClient,
);
const editor = useEditor({
extensions,
content,
editorProps: {
attributes: {
class: 'focus:outline-none w-full px-4 py-2 min-h-[40px]',
},
handleKeyDown(_, event) {
if (!editor) {
return false;
}
if (event.key === 'Enter' && !event.shiftKey) {
// check if the variable suggestion list is focused
// if it is, return false so the default behavior is not triggered
const variableSuggestionList = document.getElementById(
'variable-suggestion-list',
);
if (variableSuggestionList) {
return false;
}
event.preventDefault();
onSubmit(editor.getJSON());
return true;
}
if (event.key === 'Enter' && event.shiftKey) {
event.preventDefault();
editor.commands.insertContent([
{ type: 'text', text: ' ' },
{ type: 'paragraph' },
]);
return true;
}
return false;
},
},
onUpdate: ({ editor }) => {
editorRef.current = editor;
},
onDestroy: () => {
editorRef.current = null;
},
});
useEffect(() => {
if (!editor || !roadmapTreeData || !roadmapDetailsData) {
return;
}
editor.storage.variable.variables = roadmapTreeData.map((mapping) => {
return {
id: mapping.nodeId,
// to remove the title of the roadmap
// and only keep the path
// e.g. "Roadmap > Topic > Subtopic" -> "Topic > Subtopic"
label: mapping.text.split(' > ').slice(1).join(' > '),
};
});
}, [editor, roadmapTreeData, roadmapDetailsData]);
return (
<div className="chat-editor w-full py-1.5">
<EditorContent editor={editor} />
</div>
);
}

View File

@@ -0,0 +1,311 @@
import { mergeAttributes, Node } from '@tiptap/core';
import { type DOMOutputSpec, Node as ProseMirrorNode } from '@tiptap/pm/model';
import { PluginKey } from '@tiptap/pm/state';
import Suggestion, { type SuggestionOptions } from '@tiptap/suggestion';
// See `addAttributes` below
export interface VariableNodeAttrs {
/**
* The identifier for the selected item that was mentioned, stored as a `data-id`
* attribute.
*/
id: string | null;
/**
* The label to be rendered by the editor as the displayed text for this mentioned
* item, if provided. Stored as a `data-label` attribute. See `renderLabel`.
*/
label?: string | null;
}
export type VariableOptions<
SuggestionItem = any,
Attrs extends Record<string, any> = VariableNodeAttrs,
> = {
/**
* The HTML attributes for a mention node.
* @default {}
* @example { class: 'foo' }
*/
HTMLAttributes: Record<string, any>;
/**
* A function to render the label of a mention.
* @deprecated use renderText and renderHTML instead
* @param props The render props
* @returns The label
* @example ({ options, node }) => `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`
*/
renderLabel?: (props: {
options: VariableOptions<SuggestionItem, Attrs>;
node: ProseMirrorNode;
}) => string;
/**
* A function to render the text of a mention.
* @param props The render props
* @returns The text
* @example ({ options, node }) => `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`
*/
renderText: (props: {
options: VariableOptions<SuggestionItem, Attrs>;
node: ProseMirrorNode;
}) => string;
/**
* A function to render the HTML of a mention.
* @param props The render props
* @returns The HTML as a ProseMirror DOM Output Spec
* @example ({ options, node }) => ['span', { 'data-type': 'mention' }, `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`]
*/
renderHTML: (props: {
options: VariableOptions<SuggestionItem, Attrs>;
node: ProseMirrorNode;
}) => DOMOutputSpec;
/**
* Whether to delete the trigger character with backspace.
* @default false
*/
deleteTriggerWithBackspace: boolean;
/**
* The suggestion options.
* @default {}
* @example { char: '@', pluginKey: MentionPluginKey, command: ({ editor, range, props }) => { ... } }
*/
suggestion: Omit<SuggestionOptions<SuggestionItem, Attrs>, 'editor'>;
};
export type VariableType = {
id: string;
label: string;
};
export type VariableStorage = {
variables: VariableType[];
};
/**
* The plugin key for the variable plugin.
* @default 'variable'
*/
export const VariablePluginKey = new PluginKey('variable');
export const VariableExtension = Node.create<VariableOptions, VariableStorage>({
name: 'variable',
priority: 101,
addStorage() {
return {
variables: [],
};
},
addOptions() {
return {
HTMLAttributes: {},
renderText({ options, node }) {
return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`;
},
deleteTriggerWithBackspace: false,
renderHTML({ options, node }) {
return [
'span',
mergeAttributes(this.HTMLAttributes, options.HTMLAttributes),
`${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`,
];
},
suggestion: {
char: '@',
pluginKey: VariablePluginKey,
command: ({ editor, range, props }) => {
// increase range.to by one when the next node is of type "text"
// and starts with a space character
const nodeAfter = editor.view.state.selection.$to.nodeAfter;
const overrideSpace = nodeAfter?.text?.startsWith(' ');
if (overrideSpace) {
range.to += 1;
}
editor
.chain()
.focus()
.insertContentAt(range, [
{
type: this.name,
attrs: props,
},
{
type: 'text',
text: ' ',
},
])
.run();
// get reference to `window` object from editor element, to support cross-frame JS usage
editor.view.dom.ownerDocument.defaultView
?.getSelection()
?.collapseToEnd();
},
allow: ({ state, range }) => {
const $from = state.doc.resolve(range.from);
const type = state.schema.nodes[this.name];
const allow = !!$from.parent.type.contentMatch.matchType(type);
return allow;
},
},
};
},
group: 'inline',
inline: true,
selectable: false,
atom: true,
addAttributes() {
return {
id: {
default: null,
parseHTML: (element) => element.getAttribute('data-id'),
renderHTML: (attributes) => {
if (!attributes.id) {
return {};
}
return {
'data-id': attributes.id,
};
},
},
label: {
default: null,
parseHTML: (element) => element.getAttribute('data-label'),
renderHTML: (attributes) => {
if (!attributes.label) {
return {};
}
return {
'data-label': attributes.label,
};
},
},
};
},
parseHTML() {
return [
{
tag: `span[data-type="${this.name}"]`,
},
];
},
renderHTML({ node, HTMLAttributes }) {
if (this.options.renderLabel !== undefined) {
console.warn(
'renderLabel is deprecated use renderText and renderHTML instead',
);
return [
'span',
mergeAttributes(
{ 'data-type': this.name },
this.options.HTMLAttributes,
HTMLAttributes,
),
this.options.renderLabel({
options: this.options,
node,
}),
];
}
const mergedOptions = { ...this.options };
mergedOptions.HTMLAttributes = mergeAttributes(
{ 'data-type': this.name },
this.options.HTMLAttributes,
HTMLAttributes,
);
const html = this.options.renderHTML({
options: mergedOptions,
node,
});
if (typeof html === 'string') {
return [
'span',
mergeAttributes(
{ 'data-type': this.name },
this.options.HTMLAttributes,
HTMLAttributes,
),
html,
];
}
return html;
},
renderText({ node }) {
if (this.options.renderLabel !== undefined) {
console.warn(
'renderLabel is deprecated use renderText and renderHTML instead',
);
return this.options.renderLabel({
options: this.options,
node,
});
}
return this.options.renderText({
options: this.options,
node,
});
},
addKeyboardShortcuts() {
return {
Backspace: () =>
this.editor.commands.command(({ tr, state }) => {
let isVariable = false;
const { selection } = state;
const { empty, anchor } = selection;
if (!empty) {
return false;
}
state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => {
if (node.type.name === this.name) {
isVariable = true;
tr.insertText(
this.options.deleteTriggerWithBackspace
? ''
: this.options.suggestion.char || '',
pos,
pos + node.nodeSize,
);
return false;
}
});
return isVariable;
}),
};
},
addProseMirrorPlugins() {
return [
Suggestion({
editor: this.editor,
...this.options.suggestion,
}),
];
},
});

View File

@@ -0,0 +1,175 @@
import { ReactRenderer } from '@tiptap/react';
import type { SuggestionOptions } from '@tiptap/suggestion';
import tippy, { type GetReferenceClientRect } from 'tippy.js';
import {
forwardRef,
Fragment,
useEffect,
useImperativeHandle,
useState,
} from 'react';
import { cn } from '../../../lib/classname';
import type { VariableStorage, VariableType } from './VariableExtension';
import { ChevronRight } from 'lucide-react';
export type VariableListProps = {
command: (variable: VariableType) => void;
items: VariableType[];
} & SuggestionOptions;
export const VariableList = forwardRef((props: VariableListProps, ref) => {
const { items, command } = props;
const [selectedIndex, setSelectedIndex] = useState(0);
const selectItem = (index: number) => {
const item = props.items[index];
if (!item) {
return;
}
command(item);
};
useEffect(() => {
setSelectedIndex(0);
}, [items]);
useImperativeHandle(ref, () => ({
onKeyDown: ({ event }: { event: KeyboardEvent }) => {
if (event.key === 'ArrowUp') {
setSelectedIndex((selectedIndex + items.length - 1) % items.length);
return true;
}
if (event.key === 'ArrowDown') {
setSelectedIndex((selectedIndex + 1) % items.length);
return true;
}
if (event.key === 'Enter') {
selectItem(selectedIndex);
return true;
}
return false;
},
}));
return (
<div
id="variable-suggestion-list"
className="flex max-w-[300px] flex-col gap-0.5 overflow-auto rounded-lg border border-gray-200 bg-white p-1 shadow-sm"
>
{items.length ? (
items.map((item, index) => {
const labelParts = item?.label.split('>');
return (
<button
className={cn(
'flex items-center gap-1 truncate rounded-md p-1 px-1.5 text-left text-sm hover:bg-gray-100',
index === selectedIndex && 'bg-gray-100',
)}
key={index}
onClick={() => selectItem(index)}
>
{labelParts.map((labelPart, counter) => {
const isLast = counter === labelParts.length - 1;
return (
<Fragment key={counter}>
<span
className={cn({
'text-gray-400': !isLast,
'text-gray-900': isLast,
})}
>
{labelPart.trim()}
</span>
{!isLast && (
<ChevronRight className="inline size-3 flex-shrink-0 stroke-[1.5] text-gray-400" />
)}
</Fragment>
);
})}
</button>
);
})
) : (
<div className="rounded-md p-1 px-1.5 text-left text-sm">No result</div>
)}
</div>
);
});
VariableList.displayName = 'VariableList';
export function variableSuggestion(): Omit<SuggestionOptions, 'editor'> {
return {
items: ({ editor, query }) => {
const storage = editor.storage.variable as VariableStorage;
return storage.variables
.filter((variable) =>
variable?.label?.toLowerCase().includes(query.toLowerCase()),
)
.slice(0, 5);
},
render: () => {
let component: ReactRenderer<any>;
let popup: InstanceType<any> | null = null;
return {
onStart: (props) => {
component = new ReactRenderer(VariableList, {
props,
editor: props.editor,
});
if (!props.clientRect) {
return;
}
popup = tippy('body', {
getReferenceClientRect: props.clientRect as GetReferenceClientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: 'manual',
placement: 'top-start',
});
},
onUpdate(props) {
component.updateProps(props);
if (!props.clientRect) {
return;
}
popup[0].setProps({
getReferenceClientRect: props.clientRect,
});
},
onKeyDown(props) {
if (props.event.key === 'Escape') {
popup[0].hide();
return true;
}
return component.ref?.onKeyDown(props);
},
onExit() {
popup[0].destroy();
component.destroy();
},
};
},
};
}

View File

@@ -5,6 +5,7 @@ import { NavigationDropdown } from '../NavigationDropdown';
import { RoadmapDropdownMenu } from '../RoadmapDropdownMenu/RoadmapDropdownMenu';
import { AccountDropdown } from './AccountDropdown';
import { CourseAnnouncement } from '../SQLCourse/CourseAnnouncement';
import { Fragment } from 'react';
---
<div class='bg-slate-900 py-5 text-white sm:py-8'>

View File

@@ -0,0 +1,420 @@
import {
Brain,
Bot,
Book,
Star,
Rocket,
CheckCircle2,
Zap,
Clock,
Crown,
Users2,
Wand2,
Play,
GitPullRequest,
} from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { cn } from '../../lib/classname';
interface FeatureCardProps {
title: string;
description: string;
Icon: LucideIcon;
duration?: string;
}
function FeatureCard({
title,
description,
Icon,
duration = '2:30',
}: FeatureCardProps) {
return (
<div className="rounded-lg border border-slate-700 bg-slate-800/50 p-8 transition-colors hover:border-blue-400">
<div className="group relative mb-6 aspect-video w-full overflow-hidden rounded-lg bg-slate-900/50">
<div className="absolute inset-0 flex items-center justify-center">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-white/10 backdrop-blur-sm">
<Play className="h-6 w-6 text-white" strokeWidth={2} />
</div>
</div>
<div className="absolute right-2 bottom-2 rounded bg-black/60 px-2 py-1 text-xs text-white backdrop-blur-sm">
{duration}
</div>
</div>
<h3 className="mb-2 text-lg font-bold text-white">{title}</h3>
<p className="leading-relaxed text-slate-400">{description}</p>
</div>
);
}
function StarRating() {
return (
<div className="flex items-center">
{[...Array(5)].map((_, i) => (
<Star key={i} className="h-4 w-4 fill-current text-yellow-400" />
))}
</div>
);
}
function Testimonial({
name,
role,
content,
}: {
name: string;
role: string;
content: string;
}) {
return (
<div className="flex flex-col rounded-lg border border-slate-700 bg-slate-800/50 p-6">
<StarRating />
<p className="mt-4 mb-auto leading-relaxed text-slate-400">{content}</p>
<div className="mt-4">
<div className="font-medium text-white">{name}</div>
<div className="text-sm text-slate-500">{role}</div>
</div>
</div>
);
}
interface StatsItemProps {
icon: LucideIcon;
text: string;
}
function StatsItem(props: StatsItemProps) {
const Icon = props.icon;
return (
<div className="flex items-center gap-3">
<Icon className="h-6 w-6 text-purple-500" strokeWidth={1.5} />
<span className="text-gray-300">{props.text}</span>
</div>
);
}
interface CredibilityItemProps {
icon: LucideIcon;
iconClassName: string;
value: string;
label: string;
subLabel: string;
}
function CredibilityItem(props: CredibilityItemProps) {
const Icon = props.icon;
return (
<div className="group flex flex-col items-center text-center">
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-lg bg-slate-900/50">
<Icon
className={cn(`h-6 w-6`, props.iconClassName)}
strokeWidth={1.5}
/>
</div>
<div className="text-3xl font-bold text-white">{props.value}</div>
<div className="mt-3 text-sm font-medium text-slate-400">
{props.label}
</div>
<div className="mt-1.5 text-xs text-slate-500">{props.subLabel}</div>
</div>
);
}
export function PremiumPage() {
const handleUpgrade = () => {
alert('Upgrade functionality coming soon!');
};
return (
<div className="min-h-screen bg-gradient-to-b from-slate-900 to-black">
<div className="mx-auto max-w-4xl px-4 py-20">
{/* Hero Section */}
<div className="mx-auto mb-20 max-w-4xl">
<div className="text-center">
<div className="mb-8 inline-flex items-center gap-2 rounded-full bg-slate-800/50 px-4 py-2 text-blue-400">
<Zap className="h-4 w-4" />
<span className="text-sm font-medium">
Unlock All Premium Features
</span>
</div>
<h1 className="mb-6 text-4xl font-bold text-white md:text-5xl">
Learn Faster with AI
</h1>
<p className="mb-8 text-lg text-balance text-slate-400 md:text-xl">
Generate unlimited courses about any topic, get career guidance
and instant answers from AI, test your skills and more
</p>
<button
onClick={handleUpgrade}
className="group mx-auto block rounded-2xl bg-gradient-to-b from-indigo-600 to-indigo-700 px-8 py-4 shadow-lg transition-all hover:-translate-y-0.5 hover:shadow-xl hover:shadow-indigo-500/25"
>
<div className="flex items-center justify-center gap-3 text-lg">
<span className="font-medium text-indigo-100">
Upgrade for just
</span>
<span className="font-bold text-white">$10/month</span>
<span className="text-white transition-transform duration-200 group-hover:translate-x-1">
</span>
</div>
</button>
<p className="mt-5 flex items-center justify-center gap-2 text-sm">
<Clock className="h-3 w-3 text-white" />
<span className="text-indigo-200">
2 months free with yearly plan
</span>
</p>
</div>
</div>
{/* Stats Section */}
<div className="mb-20 flex flex-wrap items-center justify-center gap-x-10 gap-y-8">
<StatsItem icon={Users2} text="+100K Learners" />
<StatsItem icon={Bot} text="+135K Roadmaps" />
<StatsItem icon={Book} text="+90K Courses" />
<StatsItem icon={Wand2} text="+1M AI Chats" />
</div>
{/* Testimonials */}
<div className="-mx-4 mb-20 md:-mx-8 lg:-mx-16 xl:-mx-68">
<h2 className="mb-12 text-center text-3xl font-bold text-white">
What others are saying
</h2>
<div className="grid gap-6 px-4 md:grid-cols-4 md:px-8 lg:px-16">
<Testimonial
name="Gourav Khunger"
role="Full Stack Developer"
content="The AI tutor is absolutely brilliant! It's like having a senior developer available 24/7 to answer my questions."
/>
<Testimonial
name="Meabed"
role="Tech Lead"
content="The personalized learning paths and premium resources have helped my entire team stay up-to-date with the latest tech."
/>
<Testimonial
name="Mohsin Aheer"
role="Software Engineer"
content="The interactive exercises and real-world scenarios have significantly improved my problem-solving skills."
/>
<Testimonial
name="Sarah Chen"
role="Frontend Developer"
content="The AI-powered code reviews have been invaluable. I've learned so many best practices and modern patterns."
/>
<Testimonial
name="Alex Rodriguez"
role="DevOps Engineer"
content="Premium resources helped me master cloud architecture. The roadmaps are incredibly detailed and practical."
/>
<Testimonial
name="Priya Sharma"
role="Backend Developer"
content="Worth every penny! The AI assistant helped me solve complex problems and understand advanced concepts quickly."
/>
<Testimonial
name="James Wilson"
role="Mobile Developer"
content="The cross-platform development guides are exceptional. Helped me transition from native to React Native seamlessly."
/>
<Testimonial
name="Emma Thompson"
role="UI/UX Designer"
content="As a designer learning to code, the visual learning paths and interactive tutorials made the journey much easier."
/>
</div>
</div>
{/* Features Grid */}
<div className="mb-20">
<h2 className="mb-12 text-center text-3xl font-bold text-white">
Everything You Need to Succeed
</h2>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<FeatureCard
Icon={Brain}
title="AI Learning Assistant"
description="Get instant answers and personalized guidance from our advanced AI tutor, available 24/7."
/>
<FeatureCard
Icon={Bot}
title="Custom Learning Paths"
description="Follow AI-generated roadmaps tailored to your career goals and current skill level."
/>
<FeatureCard
Icon={Crown}
title="Premium Resources"
description="Access exclusive learning materials, guides, and best practices curated by experts."
/>
<FeatureCard
Icon={Clock}
title="Time-Saving Tools"
description="Save hours with AI-generated summaries and quick reference guides."
/>
<FeatureCard
Icon={Book}
title="Interactive Exercises"
description="Practice with real-world scenarios and get instant feedback on your solutions."
/>
<FeatureCard
Icon={Rocket}
title="Career Acceleration"
description="Get guidance on industry best practices and trending technologies."
/>
</div>
</div>
{/* Credibility Stats */}
<div className="mb-20">
<div className="rounded-xl border border-slate-700 bg-slate-800/50 p-8 md:p-12">
<div className="mb-8 md:mb-12">
<h2 className="text-3xl font-bold text-white md:text-4xl">
The Platform Developers Trust
</h2>
<p className="mt-2 text-lg text-slate-400">
Join millions of developers in their learning journey
</p>
</div>
<div className="grid gap-8 md:grid-cols-4">
<CredibilityItem
icon={Star}
iconClassName="text-yellow-400 fill-current"
value="#6"
label="Most Starred Project"
subLabel="Among 200M+ Repositories"
/>
<CredibilityItem
icon={Users2}
iconClassName="text-blue-400"
value="2.1M+"
label="Active Developers"
subLabel="Learning & Growing Daily"
/>
<CredibilityItem
icon={Bot}
iconClassName="text-indigo-400"
value="37K+"
label="Discord Members"
subLabel="Active Community Support"
/>
<CredibilityItem
icon={GitPullRequest}
iconClassName="text-purple-400"
value="1000+"
label="Contributors"
subLabel="Community Driven Project"
/>
</div>
</div>
</div>
{/* Pricing Section */}
<div className="mb-20">
<h2 className="mb-12 text-center text-3xl font-bold text-white">
Choose Your Plan
</h2>
<div className="mx-auto grid max-w-5xl gap-8 md:grid-cols-2">
<div className="rounded-xl border border-slate-700 bg-slate-800/50 p-8">
<h3 className="mb-4 text-2xl font-bold text-white">Monthly</h3>
<div className="mb-6">
<div className="flex items-baseline gap-2">
<span className="text-5xl font-bold text-white">$10</span>
<span className="text-slate-400">/month</span>
</div>
<p className="mt-2 text-slate-400">
Perfect for continuous learning
</p>
</div>
<button
onClick={handleUpgrade}
className="mb-8 w-full rounded-lg bg-blue-600 px-6 py-3 font-medium text-white transition-colors hover:bg-blue-700"
>
Start Monthly Plan
</button>
<ul className="space-y-4 text-slate-300">
{[
'AI Learning Assistant',
'Personalized Learning Paths',
'Interactive Exercises',
'Premium Resources',
].map((feature) => (
<li key={feature} className="flex items-start">
<CheckCircle2
className="mt-0.5 mr-3 h-5 w-5 flex-shrink-0 text-blue-400"
strokeWidth={2}
/>
<span>{feature}</span>
</li>
))}
</ul>
</div>
<div className="relative rounded-xl border-2 border-blue-400 bg-slate-800/50 p-8">
<div className="absolute -top-4 left-1/2 -translate-x-1/2 transform">
<span className="rounded-full bg-blue-600 px-4 py-1 text-sm font-medium text-white">
Most Popular
</span>
</div>
<h3 className="mb-4 text-2xl font-bold text-white">Yearly</h3>
<div className="mb-6">
<div className="flex items-baseline gap-2">
<span className="text-5xl font-bold text-white">$100</span>
<span className="text-slate-400">/year</span>
</div>
<p className="mt-2 font-medium text-blue-400">
Save $20 (2 months free)
</p>
</div>
<button
onClick={handleUpgrade}
className="mb-8 w-full rounded-lg bg-blue-600 px-6 py-3 font-medium text-white transition-colors hover:bg-blue-700"
>
Start Yearly Plan
</button>
<ul className="space-y-4 text-slate-300">
{[
'Everything in Monthly',
'Priority Support',
'Early Access Features',
'Team Collaboration Tools',
'Advanced Analytics',
].map((feature) => (
<li key={feature} className="flex items-start">
<CheckCircle2
className="mt-0.5 mr-3 h-5 w-5 flex-shrink-0 text-blue-400"
strokeWidth={2}
/>
<span>{feature}</span>
</li>
))}
</ul>
</div>
</div>
</div>
{/* Final CTA */}
<div className="mx-auto max-w-3xl text-center">
<h2 className="mb-4 text-3xl font-bold text-white">
Not Ready to Commit Yet?
</h2>
<p className="mb-8 text-lg text-slate-400">
Try our AI features for free and experience the power of AI-assisted
learning before upgrading.
</p>
<a
href="/ai"
className="group inline-flex items-center gap-3 rounded-full bg-slate-800/50 px-6 py-3 text-blue-400 ring-1 ring-slate-700/50 transition-all hover:bg-slate-800 hover:text-blue-300 hover:ring-blue-400/50"
>
<Bot className="h-5 w-5 transition-transform group-hover:scale-110" />
<span className="text-lg font-medium">
Try AI Features for Free
</span>
<span className="transform transition-transform group-hover:translate-x-1">
</span>
</a>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,48 @@
import { SettingsIcon, Trash2, type LucideIcon } from 'lucide-react';
type AIChatActionButtonProps = {
icon: LucideIcon;
label: string;
onClick: () => void;
};
function AIChatActionButton(props: AIChatActionButtonProps) {
const { icon: Icon, label, onClick } = props;
return (
<button
className="flex items-center gap-1 rounded-md border border-gray-200 px-2 py-1.5 text-xs hover:bg-gray-100"
onClick={onClick}
>
<Icon className="size-3" />
<span>{label}</span>
</button>
);
}
type AIChatActionButtonsProps = {
onTellUsAboutYourSelf: () => void;
onClearChat: () => void;
messageCount: number;
};
export function AIChatActionButtons(props: AIChatActionButtonsProps) {
const { onTellUsAboutYourSelf, onClearChat, messageCount } = props;
return (
<div className="flex gap-2 px-4 pt-2">
<AIChatActionButton
icon={SettingsIcon}
label="Tell us about your self"
onClick={onTellUsAboutYourSelf}
/>
{messageCount > 0 && (
<AIChatActionButton
icon={Trash2}
label="Clear chat"
onClick={onClearChat}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,68 @@
svg text tspan {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeSpeed;
}
svg > g[data-type='topic'],
svg > g[data-type='subtopic'],
svg g[data-type='link-item'],
svg > g[data-type='button'],
svg > g[data-type='resourceButton'],
svg > g[data-type='todo-checkbox'],
svg > g[data-type='todo'],
svg > g[data-type='checklist'] > g[data-type='checklist-item'] > rect {
cursor: pointer;
}
svg > g[data-type='topic']:hover > rect {
fill: var(--hover-color);
}
svg > g[data-type='subtopic']:hover > rect {
fill: var(--hover-color);
}
svg g[data-type='button']:hover,
svg g[data-type='link-item']:hover,
svg g[data-type='resourceButton']:hover,
svg g[data-type='todo-checkbox']:hover {
opacity: 0.8;
}
svg g[data-type='checklist'] > g[data-type='checklist-item'] > rect:hover {
fill: #cbcbcb !important;
}
svg .done rect {
fill: #cbcbcb !important;
}
svg .done text,
svg .skipped text {
text-decoration: line-through;
}
svg > g[data-type='topic'].learning > rect + text,
svg > g[data-type='topic'].done > rect + text {
fill: black;
}
svg .done text[fill='#ffffff'] {
fill: black;
}
svg > g[data-type='subtipic'].done > rect + text,
svg > g[data-type='subtipic'].learning > rect + text {
fill: #cbcbcb;
}
svg .learning rect {
fill: #dad1fd !important;
}
svg .learning text {
text-decoration: underline;
}
svg .skipped rect {
fill: #496b69 !important;
}

View File

@@ -0,0 +1,263 @@
import './ChatRoadmapRenderer.css';
import { lazy, useCallback, useEffect, useRef, useState } from 'react';
import {
renderResourceProgress,
updateResourceProgress,
type ResourceProgressType,
renderTopicProgress,
refreshProgressCounters,
} from '../../lib/resource-progress';
import { pageProgressMessage } from '../../stores/page';
import { useToast } from '../../hooks/use-toast';
import type { Edge, Node } from '@roadmapsh/editor';
import { slugify } from '../../lib/slugger';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { queryClient } from '../../stores/query-client';
import { userResourceProgressOptions } from '../../queries/resource-progress';
import { useQuery } from '@tanstack/react-query';
import { TopicResourcesModal } from './TopicResourcesModal';
const Renderer = lazy(() =>
import('@roadmapsh/editor').then((mod) => ({
default: mod.Renderer,
})),
);
type RoadmapNodeDetails = {
nodeId: string;
nodeType: string;
targetGroup: SVGElement;
title?: string;
};
function getNodeDetails(svgElement: SVGElement): RoadmapNodeDetails | null {
const targetGroup = (svgElement?.closest('g') as SVGElement) || {};
const nodeId = targetGroup?.dataset?.nodeId;
const nodeType = targetGroup?.dataset?.type;
const title = targetGroup?.dataset?.title;
if (!nodeId || !nodeType) {
return null;
}
return { nodeId, nodeType, targetGroup, title };
}
const allowedNodeTypes = [
'topic',
'subtopic',
'button',
'link-item',
'resourceButton',
'todo',
'todo-checkbox',
'checklist-item',
];
export type ChatRoadmapRendererProps = {
roadmapId: string;
nodes: Node[];
edges: Edge[];
onSelectTopic: (topicId: string, topicTitle: string) => void;
};
export function ChatRoadmapRenderer(props: ChatRoadmapRendererProps) {
const { roadmapId, nodes = [], edges = [], onSelectTopic } = props;
const roadmapRef = useRef<HTMLDivElement>(null);
const toast = useToast();
const { data: userResourceProgressData } = useQuery(
userResourceProgressOptions('roadmap', roadmapId),
queryClient,
);
async function updateTopicStatus(
topicId: string,
newStatus: ResourceProgressType,
) {
pageProgressMessage.set('Updating progress');
updateResourceProgress(
{
resourceId: roadmapId,
resourceType: 'roadmap',
topicId,
},
newStatus,
)
.then(() => {
renderTopicProgress(topicId, newStatus);
queryClient.invalidateQueries(
userResourceProgressOptions('roadmap', roadmapId),
);
})
.catch((err) => {
toast.error('Something went wrong, please try again.');
console.error(err);
})
.finally(() => {
pageProgressMessage.set('');
});
return;
}
const handleSvgClick = useCallback((e: MouseEvent) => {
const target = e.target as SVGElement;
const { nodeId, nodeType, targetGroup, title } =
getNodeDetails(target) || {};
if (!nodeId || !nodeType || !allowedNodeTypes.includes(nodeType)) {
return;
}
if (
nodeType === 'button' ||
nodeType === 'link-item' ||
nodeType === 'resourceButton'
) {
const link = targetGroup?.dataset?.link || '';
const isExternalLink = link.startsWith('http');
if (isExternalLink) {
window.open(link, '_blank');
} else {
window.location.href = link;
}
return;
}
const isCurrentStatusLearning = targetGroup?.classList.contains('learning');
const isCurrentStatusSkipped = targetGroup?.classList.contains('skipped');
if (nodeType === 'todo-checkbox') {
e.preventDefault();
if (!isLoggedIn()) {
showLoginPopup();
return;
}
const newStatus = targetGroup?.classList.contains('done')
? 'pending'
: 'done';
updateTopicStatus(nodeId, newStatus);
return;
}
if (e.shiftKey) {
e.preventDefault();
if (!isLoggedIn()) {
showLoginPopup();
return;
}
updateTopicStatus(
nodeId,
isCurrentStatusLearning ? 'pending' : 'learning',
);
return;
} else if (e.altKey) {
e.preventDefault();
if (!isLoggedIn()) {
showLoginPopup();
return;
}
updateTopicStatus(nodeId, isCurrentStatusSkipped ? 'pending' : 'skipped');
return;
}
// for the click on rect of checklist-item
if (nodeType === 'checklist-item' && target.tagName === 'rect') {
e.preventDefault();
if (!isLoggedIn()) {
showLoginPopup();
return;
}
const newStatus = targetGroup?.classList.contains('done')
? 'pending'
: 'done';
updateTopicStatus(nodeId, newStatus);
return;
}
// we don't have the topic popup for checklist-item
if (nodeType === 'checklist-item') {
return;
}
if (!title || !nodeId) {
return;
}
onSelectTopic(nodeId, title);
}, []);
const handleSvgRightClick = useCallback((e: MouseEvent) => {
e.preventDefault();
const target = e.target as SVGElement;
const { nodeId, nodeType, targetGroup } = getNodeDetails(target) || {};
if (!nodeId || !nodeType || !allowedNodeTypes.includes(nodeType)) {
return;
}
if (nodeType === 'button') {
return;
}
if (!isLoggedIn()) {
showLoginPopup();
return;
}
const isCurrentStatusDone = targetGroup?.classList.contains('done');
updateTopicStatus(nodeId, isCurrentStatusDone ? 'pending' : 'done');
}, []);
useEffect(() => {
if (!roadmapRef?.current) {
return;
}
roadmapRef?.current?.addEventListener('click', handleSvgClick);
roadmapRef?.current?.addEventListener('contextmenu', handleSvgRightClick);
return () => {
roadmapRef?.current?.removeEventListener('click', handleSvgClick);
roadmapRef?.current?.removeEventListener(
'contextmenu',
handleSvgRightClick,
);
};
}, []);
return (
<Renderer
ref={roadmapRef}
roadmap={{ nodes, edges }}
onRendered={() => {
roadmapRef.current?.setAttribute('data-renderer', 'editor');
if (!userResourceProgressData) {
return;
}
const { done, learning, skipped } = userResourceProgressData;
done.forEach((topicId) => {
renderTopicProgress(topicId, 'done');
});
learning.forEach((topicId) => {
renderTopicProgress(topicId, 'learning');
});
skipped.forEach((topicId) => {
renderTopicProgress(topicId, 'skipped');
});
}}
/>
);
}

View File

@@ -0,0 +1,141 @@
.prose ul li > code,
.prose ol li > code,
p code,
a > code,
strong > code,
em > code,
h1 > code,
h2 > code,
h3 > code {
background: #ebebeb !important;
color: currentColor !important;
font-size: 14px;
font-weight: normal !important;
}
.course-ai-content.course-content.prose ul li > code,
.course-ai-content.course-content.prose ol li > code,
.course-ai-content.course-content.prose p code,
.course-ai-content.course-content.prose a > code,
.course-ai-content.course-content.prose strong > code,
.course-ai-content.course-content.prose em > code,
.course-ai-content.course-content.prose h1 > code,
.course-ai-content.course-content.prose h2 > code,
.course-ai-content.course-content.prose h3 > code,
.course-notes-content.prose ul li > code,
.course-notes-content.prose ol li > code,
.course-notes-content.prose p code,
.course-notes-content.prose a > code,
.course-notes-content.prose strong > code,
.course-notes-content.prose em > code,
.course-notes-content.prose h1 > code,
.course-notes-content.prose h2 > code,
.course-notes-content.prose h3 > code {
font-size: 12px !important;
}
.course-ai-content pre {
-ms-overflow-style: none;
scrollbar-width: none;
}
.course-ai-content pre::-webkit-scrollbar {
display: none;
}
.course-ai-content pre,
.course-notes-content pre {
overflow: scroll;
font-size: 15px;
margin: 10px 0;
}
.prose ul li > code:before,
p > code:before,
.prose ul li > code:after,
.prose ol li > code:before,
p > code:before,
.prose ol li > code:after,
.course-content h1 > code:after,
.course-content h1 > code:before,
.course-content h2 > code:after,
.course-content h2 > code:before,
.course-content h3 > code:after,
.course-content h3 > code:before,
.course-content h4 > code:after,
.course-content h4 > code:before,
p > code:after,
a > code:after,
a > code:before {
content: '' !important;
}
.course-content.prose ul li > code,
.course-content.prose ol li > code,
.course-content p code,
.course-content a > code,
.course-content strong > code,
.course-content em > code,
.course-content h1 > code,
.course-content h2 > code,
.course-content h3 > code,
.course-content table code {
background: #f4f4f5 !important;
border: 1px solid #282a36 !important;
color: #282a36 !important;
padding: 2px 4px;
border-radius: 5px;
font-size: 16px !important;
white-space: pre;
font-weight: normal;
}
.course-content blockquote {
font-style: normal;
}
.course-content.prose blockquote h1,
.course-content.prose blockquote h2,
.course-content.prose blockquote h3,
.course-content.prose blockquote h4 {
font-style: normal;
margin-bottom: 8px;
}
.course-content.prose ul li > code:before,
.course-content p > code:before,
.course-content.prose ul li > code:after,
.course-content p > code:after,
.course-content h2 > code:after,
.course-content h2 > code:before,
.course-content table code:before,
.course-content table code:after,
.course-content a > code:after,
.course-content a > code:before,
.course-content h2 code:after,
.course-content h2 code:before,
.course-content h2 code:after,
.course-content h2 code:before {
content: '' !important;
}
.course-content table {
border-collapse: collapse;
border: 1px solid black;
border-radius: 5px;
}
.course-content table td,
.course-content table th {
padding: 5px 10px;
}
.course-ai-content .chat-variable {
font-size: 12px;
font-weight: 500;
line-height: 1.5;
padding: 2px 4px;
border-radius: 8px;
background-color: #f0f5ff;
color: #2c5df1;
}

View File

@@ -0,0 +1,717 @@
import './RoadmapAIChat.css';
import { useQuery } from '@tanstack/react-query';
import { roadmapJSONOptions } from '../../queries/roadmap';
import { queryClient } from '../../stores/query-client';
import {
Fragment,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
Bot,
Frown,
Loader2Icon,
LockIcon,
PauseCircleIcon,
SendIcon,
} from 'lucide-react';
import { ChatEditor } from '../ChatEditor/ChatEditor';
import { roadmapTreeMappingOptions } from '../../queries/roadmap-tree';
import { type AllowedAIChatRole } from '../GenerateCourse/AICourseLessonChat';
import { isLoggedIn, removeAuthToken } from '../../lib/jwt';
import type { JSONContent, Editor } from '@tiptap/core';
import { flushSync } from 'react-dom';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { readStream } from '../../lib/ai';
import { useToast } from '../../hooks/use-toast';
import { userResourceProgressOptions } from '../../queries/resource-progress';
import { ChatRoadmapRenderer } from './ChatRoadmapRenderer';
import {
renderMessage,
type MessagePartRenderer,
} from '../../lib/render-chat-message';
import { RoadmapAIChatCard } from './RoadmapAIChatCard';
import { UserProgressList } from './UserProgressList';
import { UserProgressActionList } from './UserProgressActionList';
import { RoadmapTopicList } from './RoadmapTopicList';
import { ShareResourceLink } from './ShareResourceLink';
import { RoadmapRecommendations } from './RoadmapRecommendations';
import { RoadmapAIChatHeader } from './RoadmapAIChatHeader';
import { showLoginPopup } from '../../lib/popup';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { billingDetailsOptions } from '../../queries/billing';
import { TopicDetail } from '../TopicDetail/TopicDetail';
import { slugify } from '../../lib/slugger';
import { AIChatActionButtons } from './AIChatActionButtons';
import { cn } from '../../lib/classname';
import {
getTailwindScreenDimension,
type TailwindScreenDimensions,
} from '../../lib/is-mobile';
import { ChatPersona } from '../UserPersona/ChatPersona';
import { userPersonaOptions } from '../../queries/user-persona';
import { UpdatePersonaModal } from '../UserPersona/UpdatePersonaModal';
import { lockBodyScroll } from '../../lib/dom';
import { TutorIntroMessage } from './TutorIntroMessage';
export type RoamdapAIChatHistoryType = {
role: AllowedAIChatRole;
isDefault?: boolean;
// these two will be used only into the backend
// for transforming the raw message into the final message
content?: string;
json?: JSONContent;
// these two will be used only into the frontend
// for rendering the message
html?: string;
jsx?: React.ReactNode;
};
export type RoadmapAIChatTab = 'chat' | 'topic';
type RoadmapAIChatProps = {
roadmapId: string;
};
export function RoadmapAIChat(props: RoadmapAIChatProps) {
const { roadmapId } = props;
const toast = useToast();
const editorRef = useRef<Editor | null>(null);
const scrollareaRef = useRef<HTMLDivElement>(null);
const [deviceType, setDeviceType] = useState<TailwindScreenDimensions>();
useLayoutEffect(() => {
setDeviceType(getTailwindScreenDimension());
}, []);
const [isChatMobileVisible, setIsChatMobileVisible] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const [selectedTopicId, setSelectedTopicId] = useState<string | null>(null);
const [selectedTopicTitle, setSelectedTopicTitle] = useState<string | null>(
null,
);
const [activeTab, setActiveTab] = useState<RoadmapAIChatTab>('chat');
const [aiChatHistory, setAiChatHistory] = useState<
RoamdapAIChatHistoryType[]
>([]);
const [isStreamingMessage, setIsStreamingMessage] = useState(false);
const [streamedMessage, setStreamedMessage] =
useState<React.ReactNode | null>(null);
const [showUpdatePersonaModal, setShowUpdatePersonaModal] = useState(false);
const { data: roadmapDetail, error: roadmapDetailError } = useQuery(
roadmapJSONOptions(roadmapId),
queryClient,
);
const { data: roadmapTreeData, isLoading: roadmapTreeLoading } = useQuery(
roadmapTreeMappingOptions(roadmapId),
queryClient,
);
const { isLoading: userResourceProgressLoading } = useQuery(
userResourceProgressOptions('roadmap', roadmapId),
queryClient,
);
const { data: tokenUsage, isLoading: isTokenUsageLoading } = useQuery(
getAiCourseLimitOptions(),
queryClient,
);
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient);
const { data: userPersona, isLoading: isUserPersonaLoading } = useQuery(
userPersonaOptions(roadmapId),
queryClient,
);
useEffect(() => {
lockBodyScroll(isChatMobileVisible);
}, [isChatMobileVisible]);
const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0);
const isPaidUser = userBillingDetails?.status === 'active';
const roadmapContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!roadmapDetail || !roadmapContainerRef.current) {
return;
}
roadmapContainerRef.current.replaceChildren(roadmapDetail.svg);
}, [roadmapDetail]);
useEffect(() => {
if (!roadmapTreeData || !roadmapDetail || isUserPersonaLoading) {
return;
}
setIsLoading(false);
}, [roadmapTreeData, roadmapDetail, isUserPersonaLoading]);
const abortControllerRef = useRef<AbortController | null>(null);
const handleChatSubmit = (json: JSONContent) => {
if (
!json ||
isStreamingMessage ||
!isLoggedIn() ||
isLoading ||
abortControllerRef.current
) {
return;
}
abortControllerRef.current = new AbortController();
const html = htmlFromTiptapJSON(json);
const newMessages: RoamdapAIChatHistoryType[] = [
...aiChatHistory,
{
role: 'user',
json,
html,
},
];
flushSync(() => {
setAiChatHistory(newMessages);
editorRef.current?.commands.setContent('<p></p>');
});
scrollToBottom();
completeAITutorChat(newMessages, abortControllerRef.current);
};
const scrollToBottom = useCallback(() => {
scrollareaRef.current?.scrollTo({
top: scrollareaRef.current.scrollHeight,
behavior: 'smooth',
});
}, [scrollareaRef]);
const handleSelectTopic = useCallback(
(topicId: string, topicTitle: string) => {
flushSync(() => {
setSelectedTopicId(topicId);
setSelectedTopicTitle(topicTitle);
setActiveTab('topic');
if (['sm', 'md', 'lg', 'xl'].includes(deviceType || 'xl')) {
setIsChatMobileVisible(true);
}
});
const topicWithSlug = slugify(topicTitle) + '@' + topicId;
window.dispatchEvent(
new CustomEvent('roadmap.node.click', {
detail: {
resourceType: 'roadmap',
resourceId: roadmapId,
topicId: topicWithSlug,
isCustomResource: false,
},
}),
);
},
[roadmapId, deviceType],
);
const renderer: Record<string, MessagePartRenderer> = useMemo(() => {
return {
'user-progress': () => {
return <UserProgressList roadmapId={roadmapId} />;
},
'update-progress': (options) => {
return <UserProgressActionList roadmapId={roadmapId} {...options} />;
},
'roadmap-topics': (options) => {
return (
<RoadmapTopicList
roadmapId={roadmapId}
onTopicClick={(topicId, text) => {
const title = text.split(' > ').pop();
if (!title) {
return;
}
handleSelectTopic(topicId, title);
}}
{...options}
/>
);
},
'resource-progress-link': () => {
return <ShareResourceLink roadmapId={roadmapId} />;
},
'roadmap-recommendations': (options) => {
return <RoadmapRecommendations roadmapId={roadmapId} {...options} />;
},
};
}, [roadmapId, handleSelectTopic]);
const completeAITutorChat = async (
messages: RoamdapAIChatHistoryType[],
abortController?: AbortController,
) => {
try {
setIsStreamingMessage(true);
const response = await fetch(
`${import.meta.env.PUBLIC_API_URL}/v1-chat-roadmap`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
signal: abortController?.signal,
body: JSON.stringify({
roadmapId,
messages: messages.slice(-10),
}),
},
);
if (!response.ok) {
const data = await response.json();
toast.error(data?.message || 'Something went wrong');
setAiChatHistory([...messages].slice(0, messages.length - 1));
setIsStreamingMessage(false);
if (data.status === 401) {
removeAuthToken();
window.location.reload();
}
queryClient.invalidateQueries(getAiCourseLimitOptions());
return;
}
const reader = response.body?.getReader();
if (!reader) {
setIsStreamingMessage(false);
toast.error('Something went wrong');
return;
}
await readStream(reader, {
onStream: async (content) => {
if (abortController?.signal.aborted) {
return;
}
const jsx = await renderMessage(content, renderer, {
isLoading: true,
});
flushSync(() => {
setStreamedMessage(jsx);
});
scrollToBottom();
},
onStreamEnd: async (content) => {
if (abortController?.signal.aborted) {
return;
}
const jsx = await renderMessage(content, renderer, {
isLoading: false,
});
const newMessages: RoamdapAIChatHistoryType[] = [
...messages,
{
role: 'assistant',
content,
jsx,
},
];
flushSync(() => {
setStreamedMessage(null);
setIsStreamingMessage(false);
setAiChatHistory(newMessages);
});
queryClient.invalidateQueries(getAiCourseLimitOptions());
scrollToBottom();
},
});
setIsStreamingMessage(false);
abortControllerRef.current = null;
} catch (error) {
setIsStreamingMessage(false);
setStreamedMessage(null);
abortControllerRef.current = null;
if (abortController?.signal.aborted) {
return;
}
toast.error('Something went wrong');
}
};
const handleAbort = () => {
abortControllerRef.current?.abort();
abortControllerRef.current = null;
setIsStreamingMessage(false);
setStreamedMessage(null);
setAiChatHistory([...aiChatHistory].slice(0, aiChatHistory.length - 1));
};
useEffect(() => {
scrollToBottom();
}, []);
if (roadmapDetailError) {
return (
<div className="flex flex-grow flex-col items-center justify-center">
<Frown className="mb-4 size-16" />
<h1 className="mb-2 text-2xl font-bold">There was an error</h1>
<p className="max-w-sm text-balance text-gray-500">
{roadmapDetailError.message}
</p>
</div>
);
}
const isDataLoading =
isLoading ||
roadmapTreeLoading ||
userResourceProgressLoading ||
isTokenUsageLoading ||
isBillingDetailsLoading ||
isUserPersonaLoading;
const shouldShowChatPersona =
!isLoading && !isUserPersonaLoading && !userPersona && isLoggedIn();
return (
<div className="flex flex-grow flex-row">
<div className="relative h-full flex-grow overflow-y-scroll">
{showUpgradeModal && (
<UpgradeAccountModal onClose={() => setShowUpgradeModal(false)} />
)}
{showUpdatePersonaModal && (
<UpdatePersonaModal
roadmapId={roadmapId}
onClose={() => setShowUpdatePersonaModal(false)}
/>
)}
{isLoading && (
<div className="absolute inset-0 flex h-full w-full items-center justify-center">
<Loader2Icon className="size-6 animate-spin stroke-[2.5]" />
</div>
)}
{roadmapDetail?.json && !isLoading && (
<div className="relative mx-auto max-w-[968px] px-4 pb-28 xl:pb-0">
<ChatRoadmapRenderer
roadmapId={roadmapId}
nodes={roadmapDetail?.json.nodes}
edges={roadmapDetail?.json.edges}
onSelectTopic={handleSelectTopic}
/>
{/* floating chat button */}
{!isChatMobileVisible && (
<div className="fixed bottom-4 left-1/2 z-50 block -translate-x-1/2 xl:hidden">
<button
onClick={() => {
setActiveTab('chat');
setIsChatMobileVisible(true);
}}
className="relative w-max overflow-hidden rounded-full bg-stone-900 px-4 py-2 text-center text-white shadow-2xl hover:bg-stone-800"
>
<span className="relative z-20 flex items-center gap-2 text-sm">
<Bot className="size-5 text-yellow-400" />
<span>Chat with Roadmap</span>
</span>
</button>
</div>
)}
</div>
)}
</div>
{isChatMobileVisible && (
<div
onClick={() => {
setIsChatMobileVisible(false);
}}
className="fixed inset-0 z-50 bg-black/50"
/>
)}
<div
className={cn(
'h-full flex-grow flex-col border-l border-gray-200 bg-white',
{
'relative hidden max-w-[40%] xl:flex': !isChatMobileVisible,
'fixed inset-y-0 right-0 z-50 w-full max-w-[520px]':
isChatMobileVisible,
flex: isChatMobileVisible,
},
)}
>
<RoadmapAIChatHeader
isLoading={isDataLoading}
onLogin={() => {
showLoginPopup();
}}
onUpgrade={() => {
setShowUpgradeModal(true);
}}
activeTab={activeTab}
onTabChange={(tab) => {
setActiveTab(tab);
if (tab === 'topic' && selectedTopicId && selectedTopicTitle) {
handleSelectTopic(selectedTopicId, selectedTopicTitle);
}
}}
onCloseTopic={() => {
setSelectedTopicId(null);
setSelectedTopicTitle(null);
setActiveTab('chat');
}}
onCloseChat={() => {
setIsChatMobileVisible(false);
setActiveTab('chat');
}}
selectedTopicId={selectedTopicId}
/>
{activeTab === 'topic' && selectedTopicId && (
<TopicDetail
resourceId={selectedTopicId}
resourceType="roadmap"
renderer="editor"
defaultActiveTab="content"
hasUpgradeButtons={false}
canSubmitContribution={false}
wrapperClassName="grow flex flex-col overflow-y-auto"
bodyClassName="static mx-auto h-auto grow sm:max-w-full sm:p-4"
overlayClassName="hidden"
closeButtonClassName="hidden"
onClose={() => {
setSelectedTopicId(null);
setSelectedTopicTitle(null);
setActiveTab('chat');
}}
shouldCloseOnBackdropClick={false}
shouldCloseOnEscape={false}
/>
)}
{activeTab === 'chat' && (
<>
<div className="relative grow overflow-y-auto" ref={scrollareaRef}>
{isLoading && (
<div className="absolute inset-0 flex h-full w-full items-center justify-center">
<div className="flex items-center gap-2 rounded-lg border border-gray-200 bg-white p-1.5 px-3 text-sm text-gray-500">
<Loader2Icon className="size-4 animate-spin stroke-[2.5]" />
<span>Loading Roadmap</span>
</div>
</div>
)}
{shouldShowChatPersona && !isLoading && (
<ChatPersona roadmapId={roadmapId} />
)}
{!isLoading && !shouldShowChatPersona && (
<div className="absolute inset-0 flex flex-col">
<div className="relative flex grow flex-col justify-end">
<div className="flex flex-col justify-end gap-2 px-3 py-2">
<RoadmapAIChatCard
role="assistant"
jsx={
<TutorIntroMessage roadmap={roadmapDetail?.json!} />
}
isIntro
/>
{aiChatHistory.map((chat, index) => {
return (
<Fragment key={`chat-${index}`}>
<RoadmapAIChatCard {...chat} />
</Fragment>
);
})}
{isStreamingMessage && !streamedMessage && (
<RoadmapAIChatCard
role="assistant"
html="Thinking..."
/>
)}
{streamedMessage && (
<RoadmapAIChatCard
role="assistant"
jsx={streamedMessage}
/>
)}
</div>
</div>
</div>
)}
</div>
{!isLoading && !shouldShowChatPersona && (
<div className="flex flex-col border-t border-gray-200">
{!isLimitExceeded && (
<AIChatActionButtons
onTellUsAboutYourSelf={() => {
setShowUpdatePersonaModal(true);
}}
messageCount={aiChatHistory.length}
onClearChat={() => {
setAiChatHistory([]);
}}
/>
)}
<div className="relative flex items-start text-sm">
<ChatEditor
editorRef={editorRef}
roadmapId={roadmapId}
onSubmit={(content) => {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
if (
isStreamingMessage ||
abortControllerRef.current ||
!isLoggedIn() ||
isDataLoading ||
isEmptyContent(content)
) {
return;
}
handleChatSubmit(content);
}}
/>
{isLimitExceeded && isLoggedIn() && (
<div className="absolute inset-0 z-10 flex items-center justify-center gap-2 bg-black text-white">
<LockIcon
className="size-4 cursor-not-allowed"
strokeWidth={2.5}
/>
<p className="cursor-not-allowed">
Limit reached for today
{isPaidUser ? '. Please wait until tomorrow.' : ''}
</p>
{!isPaidUser && (
<button
onClick={() => {
setShowUpgradeModal(true);
}}
className="rounded-md bg-white px-2 py-1 text-xs font-medium text-black hover:bg-gray-300"
>
Upgrade for more
</button>
)}
</div>
)}
<button
className="flex aspect-square size-[36px] items-center justify-center p-2 text-zinc-500 hover:text-black disabled:cursor-not-allowed disabled:opacity-50"
onClick={(e) => {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
if (isStreamingMessage || abortControllerRef.current) {
handleAbort();
return;
}
const json = editorRef.current?.getJSON();
if (!json || isEmptyContent(json)) {
toast.error('Please enter a message');
return;
}
handleChatSubmit(json);
}}
>
{isStreamingMessage ? (
<PauseCircleIcon className="size-4 stroke-[2.5]" />
) : (
<SendIcon className="size-4 stroke-[2.5]" />
)}
</button>
</div>
</div>
)}
</>
)}
</div>
</div>
);
}
function isEmptyContent(content: JSONContent) {
if (!content) {
return true;
}
// because they wrap the content in type doc
const firstContent = content.content?.[0];
if (!firstContent) {
return true;
}
return (
firstContent.type === 'paragraph' &&
(!firstContent?.content || firstContent?.content?.length === 0)
);
}
export function htmlFromTiptapJSON(json: JSONContent) {
const content = json.content;
let text = '';
for (const child of content || []) {
switch (child.type) {
case 'text':
text += child.text;
break;
case 'paragraph':
text += `<p>${htmlFromTiptapJSON(child)}</p>`;
break;
case 'variable':
const label = child?.attrs?.label || '';
text += `<span class="chat-variable">${label}</span>`;
break;
default:
break;
}
}
return text;
}

View File

@@ -0,0 +1,46 @@
import type { RoamdapAIChatHistoryType } from './RoadmapAIChat';
import { cn } from '../../lib/classname';
import { BotIcon, User2Icon } from 'lucide-react';
type RoadmapAIChatCardProps = RoamdapAIChatHistoryType & {
isIntro?: boolean;
};
export function RoadmapAIChatCard(props: RoadmapAIChatCardProps) {
const { role, html, jsx, isIntro = false } = props;
return (
<div
className={cn(
'flex flex-col rounded-lg',
role === 'user' ? 'bg-gray-300/30' : 'bg-yellow-500/30',
)}
>
<div className="flex items-start gap-2.5 p-3">
<div
className={cn(
'flex size-6 shrink-0 items-center justify-center rounded-full',
role === 'user'
? 'bg-gray-200 text-black'
: 'bg-yellow-400 text-black',
)}
>
{role === 'user' ? (
<User2Icon className="size-4 stroke-[2.5]" />
) : (
<BotIcon className="size-4 stroke-[2.5]" />
)}
</div>
{!!jsx && jsx}
{!!html && (
<div
className="course-content course-ai-content prose prose-sm mt-0.5 w-full max-w-[calc(100%-38px)] overflow-hidden text-sm"
dangerouslySetInnerHTML={{ __html: html }}
/>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,178 @@
import { useQuery } from '@tanstack/react-query';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import { billingDetailsOptions } from '../../queries/billing';
import { isLoggedIn } from '../../lib/jwt';
import { BookIcon, BotIcon, GiftIcon, XIcon } from 'lucide-react';
import type { RoadmapAIChatTab } from './RoadmapAIChat';
import { useState } from 'react';
import { getPercentage } from '../../lib/number';
import { AILimitsPopup } from '../GenerateCourse/AILimitsPopup';
import { cn } from '../../lib/classname';
import { useKeydown } from '../../hooks/use-keydown';
type RoadmapAIChatHeaderProps = {
isLoading: boolean;
onLogin: () => void;
onUpgrade: () => void;
onCloseChat: () => void;
activeTab: RoadmapAIChatTab;
onTabChange: (tab: RoadmapAIChatTab) => void;
onCloseTopic: () => void;
selectedTopicId: string | null;
};
type TabButtonProps = {
icon: React.ReactNode;
label: string;
isActive: boolean;
onClick: () => void;
showBorder?: boolean;
onClose?: () => void;
};
function TabButton(props: TabButtonProps) {
const { icon, label, isActive, onClick, onClose } = props;
return (
<button
className={cn(
'flex h-full flex-shrink-0 items-center gap-2 px-4 text-sm',
isActive && 'bg-gray-100',
onClose && 'pr-2 pl-4',
)}
onClick={onClick}
>
{icon}
<span className="hidden sm:block">{label}</span>
{onClose && (
<span
role="button"
className="ml-1 rounded-lg p-1 text-gray-500 hover:bg-gray-200"
onClick={(e) => {
e.stopPropagation();
onClose();
}}
>
<XIcon className="size-4 shrink-0" strokeWidth={2.5} />
</span>
)}
</button>
);
}
export function RoadmapAIChatHeader(props: RoadmapAIChatHeaderProps) {
const {
onLogin,
onUpgrade,
isLoading: isDataLoading,
onCloseChat,
activeTab,
onTabChange,
onCloseTopic,
selectedTopicId,
} = props;
const [showAILimitsPopup, setShowAILimitsPopup] = useState(false);
const { data: tokenUsage } = useQuery(getAiCourseLimitOptions(), queryClient);
const { data: userBillingDetails } = useQuery(
billingDetailsOptions(),
queryClient,
);
useKeydown('Escape', onCloseChat);
const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0);
const isPaidUser = userBillingDetails?.status === 'active';
const usagePercentage = getPercentage(
tokenUsage?.used || 0,
tokenUsage?.limit || 0,
);
const handleCreditsClick = () => {
if (!isLoggedIn()) {
onLogin();
return;
}
setShowAILimitsPopup(true);
};
const handleUpgradeClick = () => {
if (!isLoggedIn()) {
onLogin();
return;
}
onUpgrade();
};
return (
<>
{showAILimitsPopup && (
<AILimitsPopup
onClose={() => setShowAILimitsPopup(false)}
onUpgrade={() => {
setShowAILimitsPopup(false);
onUpgrade();
}}
/>
)}
<div className="flex h-[46px] flex-shrink-0 items-center justify-between border-b border-gray-200 text-sm">
<div className="flex h-full items-center">
<TabButton
icon={<BotIcon className="size-4 shrink-0 text-black" />}
label="AI Chat"
isActive={activeTab === 'chat' && !!selectedTopicId}
onClick={() => onTabChange('chat')}
/>
{(activeTab === 'topic' || selectedTopicId) && (
<TabButton
icon={<BookIcon className="size-4 shrink-0 text-black" />}
label="Topic"
isActive={activeTab === 'topic' && !!selectedTopicId}
onClick={() => onTabChange('topic')}
onClose={onCloseTopic}
/>
)}
</div>
{!isDataLoading && isLoggedIn() && (
<div className="flex gap-1.5 pr-4">
{!isPaidUser && (
<>
<button
className="hidden rounded-md bg-gray-200 px-2 py-1 text-sm hover:bg-gray-300 2xl:block"
onClick={handleCreditsClick}
>
<span className="font-medium">{usagePercentage}%</span> limit
used
</button>
<button
className="flex items-center gap-1 rounded-md bg-yellow-400 px-2 py-1 text-sm text-black hover:bg-yellow-500"
onClick={handleUpgradeClick}
>
<GiftIcon className="size-4" />
Upgrade
</button>
<button
className="hidden items-center gap-1 rounded-md bg-gray-200 px-2 py-1 text-sm text-black hover:bg-gray-300 max-xl:flex"
onClick={onCloseChat}
>
<XIcon className="size-3.5" strokeWidth={2.5} />
</button>
</>
)}
</div>
)}
</div>
</>
);
}

View File

@@ -0,0 +1,79 @@
import { useQuery } from '@tanstack/react-query';
import { queryClient } from '../../stores/query-client';
import { useMemo } from 'react';
import { listBuiltInRoadmaps } from '../../queries/roadmap';
import { SquareArrowOutUpRightIcon } from 'lucide-react';
type RoadmapSlugListType = {
roadmapSlug: string;
};
function parseRoadmapSlugList(content: string): RoadmapSlugListType[] {
const items: RoadmapSlugListType[] = [];
const roadmapSlugListRegex = /<roadmap-slug>.*?<\/roadmap-slug>/gs;
const roadmapSlugListItems = content.match(roadmapSlugListRegex);
if (!roadmapSlugListItems) {
return items;
}
for (const roadmapSlugListItem of roadmapSlugListItems) {
const roadmapSlugRegex = /<roadmap-slug>(.*?)<\/roadmap-slug>/;
const roadmapSlug = roadmapSlugListItem
.match(roadmapSlugRegex)?.[1]
?.trim();
if (!roadmapSlug) {
continue;
}
items.push({
roadmapSlug,
});
}
return items;
}
type RoadmapRecommendationsProps = {
roadmapId: string;
content: string;
};
export function RoadmapRecommendations(props: RoadmapRecommendationsProps) {
const { content } = props;
const roadmapSlugListItems = parseRoadmapSlugList(content);
const { data: roadmaps } = useQuery(listBuiltInRoadmaps(), queryClient);
const progressItemWithText = useMemo(() => {
return roadmapSlugListItems.map((item) => {
const roadmap = roadmaps?.find(
(mapping) => mapping.id === item.roadmapSlug,
);
return {
...item,
title: roadmap?.title,
};
});
}, [roadmapSlugListItems, roadmaps]);
return (
<>
<div className="relative my-6 flex flex-wrap gap-1 first:mt-0 last:mb-0">
{progressItemWithText.map((item) => (
<a
href={`/ai/chat/${item.roadmapSlug}`}
target="_blank"
key={item.roadmapSlug}
className="group flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-left text-sm text-gray-700 transition-all hover:border-gray-300 hover:bg-gray-50 hover:text-gray-900 active:bg-gray-100"
>
{item.title}
<SquareArrowOutUpRightIcon className="size-3.5 text-gray-400 transition-transform group-hover:text-gray-600" />
</a>
))}
</div>
</>
);
}

View File

@@ -0,0 +1,99 @@
import { useQuery } from '@tanstack/react-query';
import { roadmapTreeMappingOptions } from '../../queries/roadmap-tree';
import { queryClient } from '../../stores/query-client';
import { Fragment, useMemo } from 'react';
import { ChevronRightIcon } from 'lucide-react';
type TopicListType = {
topicId: string;
};
function parseTopicList(content: string): TopicListType[] {
const items: TopicListType[] = [];
const topicListRegex = /<topic-id>.*?<\/topic-id>/gs;
const topicListItems = content.match(topicListRegex);
if (!topicListItems) {
return items;
}
for (const topicListItem of topicListItems) {
const topicIdRegex = /<topic-id>(.*?)<\/topic-id>/;
const topicId = topicListItem.match(topicIdRegex)?.[1]?.trim();
if (!topicId) {
continue;
}
items.push({
topicId,
});
}
return items;
}
type RoadmapTopicListProps = {
roadmapId: string;
content: string;
onTopicClick?: (topicId: string, topicTitle: string) => void;
};
export function RoadmapTopicList(props: RoadmapTopicListProps) {
const { roadmapId, content, onTopicClick } = props;
const topicListItems = parseTopicList(content);
const { data: roadmapTreeData } = useQuery(
roadmapTreeMappingOptions(roadmapId),
queryClient,
);
const progressItemWithText = useMemo(() => {
return topicListItems.map((item) => {
const roadmapTreeItem = roadmapTreeData?.find(
(mapping) => mapping.nodeId === item.topicId,
);
return {
...item,
text: (roadmapTreeItem?.text || item.topicId)
?.split(' > ')
.slice(1)
.join(' > '),
};
});
}, [topicListItems, roadmapTreeData]);
return (
<div className="relative my-6 flex flex-wrap gap-1 first:mt-0 last:mb-0">
{progressItemWithText.map((item) => {
const labelParts = item.text.split(' > ');
const labelPartCount = labelParts.length;
return (
<button
key={item.topicId}
className="collapse-if-empty flex items-center gap-1 rounded-lg border border-gray-200 bg-white p-1 px-2 text-left text-sm hover:bg-gray-50"
onClick={() => {
onTopicClick?.(item.topicId, item.text);
}}
>
{labelParts.map((part, index) => {
return (
<Fragment key={index}>
<span>{part}</span>
{index < labelPartCount - 1 && (
<ChevronRightIcon
className="size-3 text-gray-400"
strokeWidth={2.5}
/>
)}
</Fragment>
);
})}
</button>
);
})}
</div>
);
}

View File

@@ -0,0 +1,46 @@
import { ShareIcon } from 'lucide-react';
import { useAuth } from '../../hooks/use-auth';
import { useCopyText } from '../../hooks/use-copy-text';
import { CheckIcon } from '../ReactIcons/CheckIcon';
import { cn } from '../../lib/classname';
type ShareResourceLinkProps = {
roadmapId: string;
};
export function ShareResourceLink(props: ShareResourceLinkProps) {
const { roadmapId } = props;
const user = useAuth();
const { copyText, isCopied } = useCopyText();
const handleShareResourceLink = () => {
const url = `${import.meta.env.PUBLIC_APP_URL}/${roadmapId}?s=${user?.id}`;
copyText(url);
};
return (
<div className="relative my-6 flex flex-wrap gap-1 first:mt-0 last:mb-0">
<button
className={cn(
'flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white p-1 px-1.5 text-left text-sm',
isCopied && 'text-green-500',
)}
onClick={handleShareResourceLink}
>
{!isCopied && (
<>
<ShareIcon className="h-4 w-4" />
Share Progress
</>
)}
{isCopied && (
<>
<CheckIcon additionalClasses="h-4 w-4" />
Copied
</>
)}
</button>
</div>
);
}

View File

@@ -0,0 +1,88 @@
import { useQuery } from '@tanstack/react-query';
import { Modal } from '../Modal';
import { roadmapTreeMappingOptions } from '../../queries/roadmap-tree';
import { queryClient } from '../../stores/query-client';
import { roadmapContentOptions } from '../../queries/roadmap';
import { ModalLoader } from '../UserProgress/ModalLoader';
import { TopicDetailLink } from '../TopicDetail/TopicDetailLink';
import { Spinner } from '../ReactIcons/Spinner';
import { ErrorIcon } from '../ReactIcons/ErrorIcon';
import { markdownToHtml } from '../../lib/markdown';
type TopicResourcesModalProps = {
roadmapId: string;
topicId: string;
onClose: () => void;
};
export function TopicResourcesModal(props: TopicResourcesModalProps) {
const { roadmapId, topicId, onClose } = props;
const {
data: roadmapContentData,
isLoading: isLoadingRoadmapContent,
error,
} = useQuery(roadmapContentOptions(roadmapId), queryClient);
const topicContent = roadmapContentData?.[topicId];
const links = topicContent?.links || [];
return (
<Modal onClose={onClose} wrapperClassName="max-w-lg">
{!isLoadingRoadmapContent && !error && topicContent && (
<div className="p-4">
<h2 className="text-xl font-bold">{topicContent?.title}</h2>
<div
className="course-content course-ai-content prose prose-sm mt-1 max-w-full overflow-hidden text-sm"
dangerouslySetInnerHTML={{
__html: markdownToHtml(topicContent.description, true),
}}
/>
{links.length > 0 && (
<ul className="mt-4 space-y-1">
{links.map((link, index) => {
return (
<li key={`${link.url}-${index}`}>
<TopicDetailLink
url={link.url}
type={link.type}
title={link.title}
/>
</li>
);
})}
</ul>
)}
</div>
)}
{(isLoadingRoadmapContent || error || !topicContent) && (
<div className="rounded-lg bg-white p-5">
<div className="flex items-center">
{isLoadingRoadmapContent && (
<>
<Spinner className="h-6 w-6" isDualRing={false} />
<span className="ml-3 text-lg font-semibold">
Loading Topic Resources...
</span>
</>
)}
{(error || !topicContent) && !isLoadingRoadmapContent && (
<>
<ErrorIcon additionalClasses="h-6 w-6 text-red-500" />
<span className="ml-3 text-lg font-semibold">
{!topicContent
? 'No resources found'
: (error?.message ?? 'Something went wrong')}
</span>
</>
)}
</div>
</div>
)}
</Modal>
);
}

View File

@@ -0,0 +1,100 @@
import type { RoadmapJSON } from '../../queries/roadmap';
type TutorIntroMessageProps = {
roadmap: RoadmapJSON;
};
export function TutorIntroMessage(props: TutorIntroMessageProps) {
const { roadmap } = props;
const topicNodes = roadmap.nodes.filter((node) => node.type === 'topic');
const firstTopicNode = topicNodes[0];
const firstTopicTitle = firstTopicNode?.data?.label || 'XYZ';
const secondTopicNode = topicNodes[1];
const secondTopicTitle = secondTopicNode?.data?.label || 'XYZ';
const capabilities = [
{
icon: '📚',
title: 'Learn concepts:',
description: 'Ask me about any topics on the roadmap',
examples:
'"Explain what React hooks are" or "How does async/await work?"',
},
{
icon: '📊',
title: 'Track progress:',
description: 'Mark topics as done, learning, or skipped',
examples: `"Mark ${firstTopicTitle} as done" or "Show my overall progress"`,
},
{
icon: '🎯',
title: 'Recommendations:',
description: 'Find what to learn next or explore other roadmaps',
examples: `"What should I learn next?" or "Recommend roadmaps for backend development"`,
},
{
icon: '🔍',
title: 'Find resources:',
description: 'Get learning materials for specific topics',
examples: `"Show me resources for learning ${secondTopicTitle}"`,
},
{
icon: '🔗',
title: 'Share progress:',
description: 'Get a link to share your learning progress',
examples: '"Give me my shareable progress link"',
},
];
return (
<div className="space-y-2 text-sm text-gray-700">
<div className="flex items-start gap-3">
<div>
<h3 className="mb-2 font-medium text-gray-900">
Hi! I'm your AI learning assistant 👋
</h3>
<p className="mb-3">
I'm here to guide you through your learning journey on this roadmap.
I can help you understand concepts, track your progress, and provide
personalized learning advice.
</p>
</div>
</div>
<div className="space-y-3">
<h4 className="font-medium text-gray-900">
Here's what I can help you with:
</h4>
<div className="space-y-3">
{capabilities.map((capability, index) => (
<div key={index} className="flex items-start gap-2">
<span className={`font-medium`}>{capability.icon}</span>
<div>
<span className="font-medium text-black">
{capability.title}
</span>{' '}
{capability.description}
<div className="mt-1 text-xs text-gray-600">
Try: {capability.examples}
</div>
</div>
</div>
))}
</div>
</div>
<div className="mt-4 rounded-lg bg-gray-50 p-3">
<p className="text-xs text-black">
<span className="font-medium">Tip:</span> I can see your current
progress on the roadmap, so my advice will be personalized to your
learning journey. Just ask me anything about the topics you see on the
roadmap!
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,332 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { roadmapTreeMappingOptions } from '../../queries/roadmap-tree';
import { queryClient } from '../../stores/query-client';
import { Fragment, useMemo, useState } from 'react';
import { renderTopicProgress } from '../../lib/resource-progress';
import { updateResourceProgress } from '../../lib/resource-progress';
import { pageProgressMessage } from '../../stores/page';
import type { ResourceProgressType } from '../../lib/resource-progress';
import { userResourceProgressOptions } from '../../queries/resource-progress';
import { useToast } from '../../hooks/use-toast';
import { Check, ChevronRightIcon, Loader2Icon } from 'lucide-react';
import { CheckIcon } from '../ReactIcons/CheckIcon';
import { httpPost } from '../../lib/query-http';
import { cn } from '../../lib/classname';
type UpdateUserProgress = {
id: string;
action: 'done' | 'learning' | 'skipped' | 'pending';
};
function parseUserProgress(content: string): UpdateUserProgress[] {
const items: UpdateUserProgress[] = [];
const progressRegex = /<update-progress-item>.*?<\/update-progress-item>/gs;
const progressItems = content.match(progressRegex);
if (!progressItems) {
return items;
}
for (const progressItem of progressItems) {
const progressItemRegex = /<topic-id>(.*?)<\/topic-id>/;
const topicId = progressItem.match(progressItemRegex)?.[1]?.trim();
const topicActionRegex = /<topic-action>(.*?)<\/topic-action>/;
const topicAction = progressItem
.match(topicActionRegex)?.[1]
.trim()
?.toLowerCase();
if (!topicId || !topicAction) {
continue;
}
items.push({
id: topicId,
action: topicAction as UpdateUserProgress['action'],
});
}
return items;
}
type BulkUpdateResourceProgressBody = {
done: string[];
learning: string[];
skipped: string[];
pending: string[];
};
type BulkUpdateResourceProgressResponse = {
done: string[];
learning: string[];
skipped: string[];
};
type UserProgressActionListProps = {
roadmapId: string;
content: string;
isLoading?: boolean;
};
export function UserProgressActionList(props: UserProgressActionListProps) {
const { roadmapId, content, isLoading = false } = props;
const toast = useToast();
const updateUserProgress = parseUserProgress(content);
const { data: roadmapTreeData } = useQuery(
roadmapTreeMappingOptions(roadmapId),
queryClient,
);
const {
mutate: bulkUpdateResourceProgress,
isPending: isBulkUpdating,
isSuccess: isBulkUpdateSuccess,
} = useMutation(
{
mutationFn: (body: BulkUpdateResourceProgressBody) => {
return httpPost<BulkUpdateResourceProgressResponse>(
`/v1-bulk-update-resource-progress/${roadmapId}`,
body,
);
},
onSuccess: () => {
return queryClient.invalidateQueries(
userResourceProgressOptions('roadmap', roadmapId),
);
},
onSettled: () => {
pageProgressMessage.set('');
},
onError: (error) => {
toast.error(
error?.message ?? 'Something went wrong, please try again.',
);
},
},
queryClient,
);
const progressItemWithText = useMemo(() => {
return updateUserProgress.map((item) => {
const roadmapTreeItem = roadmapTreeData?.find(
(mapping) => mapping.nodeId === item.id,
);
return {
...item,
text: (roadmapTreeItem?.text || item.id)
?.split(' > ')
.slice(1)
.join(' > '),
};
});
}, [updateUserProgress, roadmapTreeData]);
const [showAll, setShowAll] = useState(false);
const itemCountToShow = 4;
const itemsToShow = showAll
? progressItemWithText
: progressItemWithText.slice(0, itemCountToShow);
const hasMoreItemsToShow = progressItemWithText.length > itemCountToShow;
return (
<div className="relative my-6 w-full first:mt-0 last:mb-0">
<div className="relative flex flex-col gap-0.5">
{itemsToShow.map((item) => (
<ProgressItem
key={item.id}
roadmapId={roadmapId}
topicId={item.id}
text={item.text}
action={item.action}
isStreaming={isLoading}
isBulkUpdating={isBulkUpdating}
isBulkUpdateSuccess={isBulkUpdateSuccess}
/>
))}
{hasMoreItemsToShow && (
<div className="relative mt-1 flex items-center justify-between gap-2">
<button
className="z-50 flex items-center gap-1 rounded-md bg-gray-400 px-2 py-1 text-xs font-medium text-white hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-70"
onClick={() => setShowAll(!showAll)}
disabled={isLoading}
>
{isLoading && (
<>
<Loader2Icon className="size-3 animate-spin" />
{progressItemWithText.length} loaded ..
</>
)}
{!isLoading && (
<>
{showAll
? '- Show Less'
: `+ Show ${progressItemWithText.length - itemCountToShow} More`}
</>
)}
</button>
<button
className="z-50 flex items-center gap-1 rounded-md bg-green-600 px-2 py-1 text-xs font-medium text-white hover:bg-green-700 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-70"
disabled={isBulkUpdating || isLoading}
onClick={() => {
const done = updateUserProgress
.filter((item) => item.action === 'done')
.map((item) => item.id);
const learning = updateUserProgress
.filter((item) => item.action === 'learning')
.map((item) => item.id);
const skipped = updateUserProgress
.filter((item) => item.action === 'skipped')
.map((item) => item.id);
const pending = updateUserProgress
.filter((item) => item.action === 'pending')
.map((item) => item.id);
bulkUpdateResourceProgress({
done,
learning,
skipped,
pending,
});
}}
>
{isBulkUpdating && (
<Loader2Icon className="size-3 animate-spin" />
)}
{!isBulkUpdating && <CheckIcon additionalClasses="size-3" />}
Apply All
</button>
</div>
)}
</div>
</div>
);
}
type ProgressItemProps = {
roadmapId: string;
topicId: string;
text: string;
action: UpdateUserProgress['action'];
isStreaming: boolean;
isBulkUpdating: boolean;
isBulkUpdateSuccess: boolean;
};
function ProgressItem(props: ProgressItemProps) {
const {
roadmapId,
topicId,
text,
action,
isStreaming,
isBulkUpdating,
isBulkUpdateSuccess,
} = props;
const toast = useToast();
const {
mutate: updateTopicStatus,
isSuccess,
isPending: isUpdating,
} = useMutation(
{
mutationFn: (action: ResourceProgressType) => {
return updateResourceProgress(
{
resourceId: roadmapId,
resourceType: 'roadmap',
topicId,
},
action,
);
},
onMutate: () => {},
onSuccess: () => {
renderTopicProgress(topicId, action);
},
onError: () => {
toast.error('Something went wrong, please try again.');
},
onSettled: () => {
pageProgressMessage.set('');
return queryClient.invalidateQueries(
userResourceProgressOptions('roadmap', roadmapId),
);
},
},
queryClient,
);
const textParts = text.split(' > ');
const lastIndex = textParts.length - 1;
return (
<div className="flex min-h-[40px] items-center justify-between gap-2 rounded-lg border border-gray-200 bg-white py-1 pr-1 pl-3">
<span className="flex items-center gap-1 truncate text-sm text-gray-500">
{textParts.map((part, index) => {
return (
<Fragment key={index}>
{part}
{index !== lastIndex && (
<span className="text-gray-500">
<ChevronRightIcon className="size-3 shrink-0" />{' '}
</span>
)}
</Fragment>
);
})}
</span>
{!isSuccess && !isBulkUpdateSuccess && (
<>
{!isStreaming && (
<button
className={cn(
`flex shrink-0 items-center gap-1.5 rounded-md border border-gray-200 px-2 py-1 text-xs disabled:pointer-events-none disabled:opacity-40`,
{
'bg-green-100 hover:border-green-300 hover:bg-green-200':
action === 'done',
'bg-yellow-100 hover:border-yellow-300 hover:bg-yellow-200':
action === 'learning',
'bg-gray-800 text-white hover:border-black hover:bg-black':
action === 'skipped',
'bg-gray-100 hover:border-gray-300 hover:bg-gray-200':
action === 'pending',
},
)}
onClick={() => updateTopicStatus(action)}
disabled={isStreaming || isUpdating || isBulkUpdating}
>
{(isUpdating || isBulkUpdating) && (
<Loader2Icon className="size-4 animate-spin" />
)}
{!isUpdating && !isBulkUpdating && (
<>
<Check strokeWidth={3} className="size-3" />
Mark it as {action}
</>
)}
</button>
)}
{isStreaming && (
<span className="flex size-[30px] items-center justify-center text-gray-300">
<Loader2Icon className="size-4 animate-spin" />
</span>
)}
</>
)}
{(isSuccess || isBulkUpdateSuccess) && (
<span className="flex size-[30px] items-center justify-center text-green-500">
<CheckIcon additionalClasses="size-4" />
</span>
)}
</div>
);
}

View File

@@ -0,0 +1,58 @@
import { useQuery } from '@tanstack/react-query';
import { queryClient } from '../../stores/query-client';
import { userResourceProgressOptions } from '../../queries/resource-progress';
import { getPercentage } from '../../lib/number';
type UserProgressListProps = {
roadmapId: string;
};
export function UserProgressList(props: UserProgressListProps) {
const { roadmapId } = props;
const { data: userResourceProgressData } = useQuery(
userResourceProgressOptions('roadmap', roadmapId),
queryClient,
);
const totalTopicCount = userResourceProgressData?.totalTopicCount ?? 0;
const doneCount = userResourceProgressData?.done?.length ?? 0;
const skippedCount = userResourceProgressData?.skipped?.length ?? 0;
const totalFinished = doneCount + skippedCount;
const progressPercentage = getPercentage(totalFinished, totalTopicCount);
return (
<div className="relative my-6 flex flex-col gap-3 rounded-xl border border-gray-200 bg-white p-4 first:mt-0 last:mb-0">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-600">Progress</span>
<span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-700">
{progressPercentage}%
</span>
</div>
<span className="text-sm hidden md:block font-medium text-gray-600">
{totalFinished} / {totalTopicCount} topics
</span>
</div>
<div className="relative h-2 w-full overflow-hidden rounded-full bg-gray-100">
<div
className="absolute inset-0 bg-gradient-to-r from-green-500 to-green-600 transition-all duration-300"
style={{ width: `${progressPercentage}%` }}
/>
</div>
<div className="flex items-center gap-4 text-xs text-gray-500">
<div className="flex items-center gap-1">
<div className="h-2 w-2 rounded-full bg-green-500" />
<span>Completed: {doneCount}</span>
</div>
<div className="flex items-center gap-1">
<div className="h-2 w-2 rounded-full bg-gray-400" />
<span>Skipped: {skippedCount}</span>
</div>
</div>
</div>
);
}

View File

@@ -2,19 +2,20 @@
import {
ArrowLeftIcon,
BookOpenIcon,
Bot,
FolderKanbanIcon,
MapIcon,
MessageCircle,
} from 'lucide-react';
import { TabLink } from './TabLink';
import LoginPopup from './AuthenticationFlow/LoginPopup.astro';
import { ScheduleButton } from './Schedule/ScheduleButton';
import ProgressHelpPopup from './ProgressHelpPopup.astro';
import { MarkFavorite } from './FeaturedItems/MarkFavorite';
import { type RoadmapFrontmatter } from '../lib/roadmap';
import { ShareRoadmapButton } from './ShareRoadmapButton';
import LoginPopup from './AuthenticationFlow/LoginPopup.astro';
import { DownloadRoadmapButton } from './DownloadRoadmapButton';
import { CourseAnnouncement } from './SQLCourse/CourseAnnouncement';
import { MarkFavorite } from './FeaturedItems/MarkFavorite';
import ProgressHelpPopup from './ProgressHelpPopup.astro';
import { ScheduleButton } from './Schedule/ScheduleButton';
import { ShareRoadmapButton } from './ShareRoadmapButton';
import { TabLink } from './TabLink';
export interface Props {
title: string;
description: string;
@@ -29,6 +30,7 @@ export interface Props {
hasSearch?: boolean;
projectCount?: number;
coursesCount?: number;
hasAIChat?: boolean;
question?: RoadmapFrontmatter['question'];
hasTopics?: boolean;
isForkable?: boolean;
@@ -43,6 +45,7 @@ const {
isUpcoming = false,
note,
hasTopics = false,
hasAIChat = false,
projectCount = 0,
question,
activeTab = 'roadmap',
@@ -150,6 +153,16 @@ const hasProjects = projectCount > 0;
badgeText='New'
/>
)}
{hasAIChat && (
<TabLink
url={`/${roadmapId}/ai`}
icon={Bot}
text='Chat with AI'
mobileText="AI"
isActive={false}
badgeText='New'
/>
)}
</div>
<TabLink

View File

@@ -0,0 +1,30 @@
import { ChevronDownIcon } from 'lucide-react';
import * as React from 'react';
import { cn } from '../lib/classname';
export function SelectNative(props: React.ComponentProps<'select'>) {
const { className, children, ...rest } = props;
return (
<div className="relative flex">
<select
data-slot="select-native"
className={cn(
'peer inline-flex w-full cursor-pointer appearance-none items-center rounded-lg border border-gray-200 text-sm text-black outline-none focus-visible:border-gray-500 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 has-[option[disabled]:checked]:text-gray-500 aria-invalid:border-red-500 aria-invalid:ring-red-500/20 dark:aria-invalid:ring-red-500/40',
props.multiple
? '[&_option:checked]:bg-accent py-1 *:px-3 *:py-1'
: 'h-9 ps-3 pe-8',
className,
)}
{...rest}
>
{children}
</select>
{!props.multiple && (
<span className="pointer-events-none absolute inset-y-0 end-0 flex h-full w-9 items-center justify-center text-gray-500/80 peer-disabled:opacity-50 peer-aria-invalid:text-red-500/80">
<ChevronDownIcon size={16} aria-hidden="true" />
</span>
)}
</div>
);
}

View File

@@ -4,6 +4,7 @@ import { cn } from '../lib/classname.ts';
type TabLinkProps = {
icon: LucideIcon;
text: string;
mobileText?: string;
isActive: boolean;
isExternal?: boolean;
badgeText?: string;
@@ -19,6 +20,7 @@ export function TabLink(props: TabLinkProps) {
isExternal = false,
url,
text,
mobileText,
isActive,
hideTextOnMobile = false,
className: additionalClassName = '',
@@ -75,7 +77,8 @@ export function TabLink(props: TabLinkProps) {
className={className}
>
<Icon className="h-4 w-4 shrink-0" />
<span className={textClass}>{text}</span>
<span className={cn(textClass, 'hidden sm:inline')}>{text}</span>
<span className={cn(textClass, 'inline sm:hidden')}>{mobileText || text}</span>
{badgeNode}
</a>
);

View File

@@ -44,16 +44,6 @@ import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal.tsx';
import { TopicProgressButton } from './TopicProgressButton.tsx';
import { CreateCourseModal } from './CreateCourseModal.tsx';
type TopicDetailProps = {
resourceId?: string;
resourceTitle?: string;
resourceType?: ResourceType;
renderer?: AllowedRoadmapRenderer;
isEmbed?: boolean;
canSubmitContribution: boolean;
};
type PaidResourceType = {
_id?: string;
title: string;
@@ -93,13 +83,41 @@ async function fetchRoadmapPaidResources(roadmapId: string) {
const PAID_RESOURCE_DISCLAIMER_HIDDEN = 'paid-resource-disclaimer-hidden';
type TopicDetailProps = {
resourceId?: string;
resourceType?: ResourceType;
renderer?: AllowedRoadmapRenderer;
defaultActiveTab?: AllowedTopicDetailsTabs;
hasUpgradeButtons?: boolean;
isEmbed?: boolean;
canSubmitContribution: boolean;
wrapperClassName?: string;
bodyClassName?: string;
overlayClassName?: string;
closeButtonClassName?: string;
onClose?: () => void;
shouldCloseOnBackdropClick?: boolean;
shouldCloseOnEscape?: boolean;
};
export function TopicDetail(props: TopicDetailProps) {
const {
hasUpgradeButtons = true,
canSubmitContribution,
resourceId: defaultResourceId,
isEmbed = false,
renderer = 'balsamiq',
resourceTitle,
wrapperClassName,
bodyClassName,
overlayClassName,
closeButtonClassName,
onClose,
shouldCloseOnBackdropClick = true,
shouldCloseOnEscape = true,
defaultActiveTab = 'content',
} = props;
const [hasEnoughLinks, setHasEnoughLinks] = useState(false);
@@ -114,7 +132,7 @@ export function TopicDetail(props: TopicDetailProps) {
const [topicHtmlTitle, setTopicHtmlTitle] = useState('');
const [links, setLinks] = useState<RoadmapContentDocument['links']>([]);
const [activeTab, setActiveTab] =
useState<AllowedTopicDetailsTabs>('content');
useState<AllowedTopicDetailsTabs>(defaultActiveTab);
const [aiChatHistory, setAiChatHistory] =
useState<AIChatHistoryType[]>(defaultChatHistory);
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
@@ -138,6 +156,7 @@ export function TopicDetail(props: TopicDetailProps) {
const [paidResources, setPaidResources] = useState<PaidResourceType[]>([]);
const handleClose = () => {
onClose?.();
setIsActive(false);
setShowUpgradeModal(false);
setAiChatHistory(defaultChatHistory);
@@ -146,8 +165,11 @@ export function TopicDetail(props: TopicDetailProps) {
};
// Close the topic detail when user clicks outside the topic detail
useOutsideClick(topicRef, handleClose);
useKeydown('Escape', handleClose);
useOutsideClick(
topicRef,
shouldCloseOnBackdropClick ? handleClose : undefined,
);
useKeydown('Escape', shouldCloseOnEscape ? handleClose : undefined);
useEffect(() => {
if (resourceType !== 'roadmap' || !defaultResourceId) {
@@ -349,7 +371,9 @@ export function TopicDetail(props: TopicDetailProps) {
});
useEffect(() => {
if (isActive) topicRef?.current?.focus();
if (isActive) {
topicRef?.current?.focus();
}
lockBodyScroll(isActive);
}, [isActive]);
@@ -370,11 +394,14 @@ export function TopicDetail(props: TopicDetailProps) {
const shouldShowAiTab = !isCustomResource && resourceType === 'roadmap';
return (
<div className={'relative z-92'}>
<div className={cn('relative z-92', wrapperClassName)}>
<div
ref={topicRef}
tabIndex={0}
className="fixed top-0 right-0 z-40 flex h-screen w-full flex-col overflow-y-auto bg-white p-4 focus:outline-0 sm:max-w-[600px] sm:p-6"
className={cn(
'fixed top-0 right-0 z-40 flex h-screen w-full flex-col overflow-y-auto bg-white p-4 focus:outline-0 sm:max-w-[600px] sm:p-6',
bodyClassName,
)}
>
{showUpgradeModal && (
<UpgradeAccountModal onClose={() => setShowUpgradeModal(false)} />
@@ -427,13 +454,16 @@ export function TopicDetail(props: TopicDetailProps) {
}
resourceId={resourceId}
resourceType={resourceType}
onClose={handleClose}
onClose={() => null}
/>
)}
<button
type="button"
id="close-topic"
className="flex items-center gap-1.5 rounded-lg bg-gray-200 px-1.5 py-1 text-xs text-black hover:bg-gray-300 hover:text-gray-900"
className={cn(
'flex items-center gap-1.5 rounded-lg bg-gray-200 px-1.5 py-1 text-xs text-black hover:bg-gray-300 hover:text-gray-900',
closeButtonClassName,
)}
onClick={handleClose}
>
<X className="size-4" />
@@ -448,6 +478,7 @@ export function TopicDetail(props: TopicDetailProps) {
topicId={topicId}
aiChatHistory={aiChatHistory}
setAiChatHistory={setAiChatHistory}
hasUpgradeButtons={hasUpgradeButtons}
onUpgrade={() => setShowUpgradeModal(true)}
onLogin={() => {
handleClose();
@@ -593,7 +624,7 @@ export function TopicDetail(props: TopicDetailProps) {
<a
href={contributionUrl}
target="_blank"
className="hidden transition-all items-center justify-center rounded-md px-2 py-2 text-sm hover:bg-gray-200 sm:flex"
className="hidden items-center justify-center rounded-md px-2 py-2 text-sm transition-all hover:bg-gray-200 sm:flex"
>
<GitHubIcon className="mr-2 inline-block h-4 w-4 text-current" />
Help us Improve this Content
@@ -642,7 +673,9 @@ export function TopicDetail(props: TopicDetailProps) {
</>
)}
</div>
<div className="fixed inset-0 z-30 bg-gray-900/50"></div>
<div
className={cn('fixed inset-0 z-30 bg-gray-900/50', overlayClassName)}
></div>
</div>
);
}

View File

@@ -36,6 +36,8 @@ type TopicDetailAIProps = {
resourceType: ResourceType;
topicId: string;
hasUpgradeButtons?: boolean;
aiChatHistory: AIChatHistoryType[];
setAiChatHistory: (history: AIChatHistoryType[]) => void;
@@ -52,6 +54,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
resourceId,
resourceType,
topicId,
hasUpgradeButtons = true,
onUpgrade,
onLogin,
onShowSubjectSearchModal,
@@ -303,7 +306,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
<button
onClick={onShowSubjectSearchModal}
className="flex text-gray-400 items-center gap-1.5 rounded-md border border-dashed hover:border-solid border-gray-300 bg-transparent px-2 py-1 hover:bg-gray-200 hover:text-black"
className="flex items-center gap-1.5 rounded-md border border-dashed border-gray-300 bg-transparent px-2 py-1 text-gray-400 hover:border-solid hover:bg-gray-200 hover:text-black"
>
<WandSparkles className="h-3 w-3" />
Learn another topic
@@ -351,7 +354,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
</button>
)}
{!isPaidUser && (
{!isPaidUser && hasUpgradeButtons && (
<>
<button
className="hidden rounded-md bg-gray-200 px-2 py-1 text-sm hover:bg-gray-300 sm:block"
@@ -471,6 +474,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
)}
</div>
)}
{!isLoggedIn() && (
<div className="absolute inset-0 z-10 flex items-center justify-center gap-2 bg-black text-white">
<LockIcon className="size-4 cursor-not-allowed" strokeWidth={2.5} />

View File

@@ -54,7 +54,7 @@ function TopicDetailsTab(props: TopicDetailsTabProps) {
<Icon className="h-4 w-4" />
<span className="hidden sm:block">{label}</span>
{isNew && !isDisabled && (
<span className="hidden rounded-sm bg-yellow-400 px-1 text-xs text-black sm:block">
<span className="hidden rounded-sm bg-yellow-400 px-1 text-xs text-black 2xl:block">
New
</span>
)}

View File

@@ -14,9 +14,10 @@ import type {
} from '../../lib/resource-progress';
import { showLoginPopup } from '../../lib/popup';
import { useToast } from '../../hooks/use-toast';
import { Spinner } from '../ReactIcons/Spinner';
import { ChevronDown } from 'lucide-react';
import { ChevronDown, Loader2 } from 'lucide-react';
import { cn } from '../../lib/classname';
import { queryClient } from '../../stores/query-client';
import { userResourceProgressOptions } from '../../queries/resource-progress';
const statusColors: Record<ResourceProgressType, string> = {
done: 'bg-green-500',
@@ -179,6 +180,8 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
);
const handleUpdateResourceProgress = (progress: ResourceProgressType) => {
setShowChangeStatus(false);
if (isGuest) {
onClose();
showLoginPopup();
@@ -209,6 +212,9 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
.finally(() => {
setShowChangeStatus(false);
setIsUpdatingProgress(false);
queryClient.invalidateQueries(
userResourceProgressOptions(resourceType, resourceId),
);
});
};
@@ -225,32 +231,38 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
progress,
);
if (isUpdatingProgress) {
return (
<button className="inline-flex cursor-default items-center rounded-md border border-gray-300 bg-white p-1 px-2 text-sm text-black">
<Spinner isDualRing={false} className="h-4 w-4" />
<span className="ml-2">Please wait..</span>
</button>
);
}
return (
<div className="relative inline-flex">
<button
disabled={isUpdatingProgress}
className={cn(
'flex cursor-default cursor-pointer items-center rounded-md border border-gray-300 p-1 px-2 text-sm text-black hover:border-gray-400',
'flex 2xl:min-w-[135px] cursor-default cursor-pointer items-center rounded-md border border-gray-300 p-1 px-2 text-sm text-black hover:border-gray-400 disabled:pointer-events-none disabled:opacity-50',
)}
onClick={() => setShowChangeStatus(true)}
>
{!isUpdatingProgress && (
<>
<span className="flex h-2 w-2">
<span
className={`relative inline-flex h-2 w-2 rounded-full ${statusColors[progress]}`}
className={cn(
'relative inline-flex h-2 w-2 rounded-full',
statusColors[progress],
isUpdatingProgress && 'animate-pulse',
)}
></span>
</span>
<span className="ml-2 capitalize">
<span className="ml-2 mr-2 capitalize">
{progress === 'learning' ? 'In Progress' : progress}
</span>
<ChevronDown className="ml-2 h-4 w-4" />
</>
)}
{isUpdatingProgress && (
<span className="flex items-center">
<Loader2 strokeWidth={3} className="size-3 flex-shrink-0 animate-spin" />
<span className="ml-2 text-sm">Updating..</span>
</span>
)}
<ChevronDown className="ml-auto h-4 w-4" />
</button>
{showChangeStatus && (

View File

@@ -0,0 +1,81 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { UserPersonaForm, type UserPersonaFormData } from './UserPersonaForm';
import { roadmapJSONOptions } from '../../queries/roadmap';
import { queryClient } from '../../stores/query-client';
import { httpPost } from '../../lib/query-http';
import { useToast } from '../../hooks/use-toast';
import { userPersonaOptions } from '../../queries/user-persona';
type ChatPersonaProps = {
roadmapId: string;
};
export function ChatPersona(props: ChatPersonaProps) {
const { roadmapId } = props;
const toast = useToast();
const { data: roadmap } = useQuery(
roadmapJSONOptions(roadmapId),
queryClient,
);
const { mutate: createUserPersona, isPending: isCreatingUserPersona } =
useMutation(
{
mutationFn: async (data: UserPersonaFormData) => {
return httpPost('/v1-set-user-persona', {
...data,
roadmapId,
});
},
onError: (error) => {
toast.error(error?.message || 'Something went wrong');
},
onSettled: () => {
return queryClient.invalidateQueries(userPersonaOptions(roadmapId));
},
},
queryClient,
);
const roadmapTitle = roadmap?.json.title ?? '';
return (
<div className="relative mx-auto flex h-auto px-4 sm:h-full max-w-[400px] grow flex-col justify-center p-4 sm:p-4 px-2">
<div className="mb-4 sm:mb-8 text-left sm:text-center">
<img
src="/images/gifs/wave.gif"
alt="Wave"
className="hidden sm:block mx-auto mb-3 sm:mb-5 h-16 sm:h-24 w-16 sm:w-24"
/>
<h2 className="text-lg sm:text-xl font-semibold">Welcome to the AI Tutor</h2>
<p className="mt-1 text-xs sm:text-sm text-balance text-gray-500 pr-8 sm:px-0">
Before we start, answer these questions so we can help you better.
</p>
</div>
<UserPersonaForm
roadmapTitle={roadmapTitle}
onSubmit={(data) => {
const trimmedGoal = data?.goal?.trim();
if (!trimmedGoal) {
toast.error('Please describe your goal');
return;
}
const trimmedCommit = data?.commit?.trim();
if (!trimmedCommit) {
toast.error(
'Please enter how many hours per week you can commit to learning',
);
return;
}
createUserPersona(data);
}}
isLoading={isCreatingUserPersona}
/>
</div>
);
}

View File

@@ -0,0 +1,92 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { userPersonaOptions } from '../../queries/user-persona';
import { queryClient } from '../../stores/query-client';
import { roadmapJSONOptions } from '../../queries/roadmap';
import { Modal } from '../Modal';
import { UserPersonaForm, type UserPersonaFormData } from './UserPersonaForm';
import { httpPost } from '../../lib/query-http';
import { useToast } from '../../hooks/use-toast';
type UpdatePersonaModalProps = {
roadmapId: string;
onClose: () => void;
};
export function UpdatePersonaModal(props: UpdatePersonaModalProps) {
const { roadmapId, onClose } = props;
const toast = useToast();
const { data: roadmap } = useQuery(
roadmapJSONOptions(roadmapId),
queryClient,
);
const { data: userPersona } = useQuery(
userPersonaOptions(roadmapId),
queryClient,
);
const { mutate: setUserPersona, isPending: isSettingUserPersona } =
useMutation(
{
mutationFn: async (data: UserPersonaFormData) => {
return httpPost('/v1-set-user-persona', {
...data,
roadmapId,
});
},
onError: (error) => {
toast.error(error?.message || 'Something went wrong');
},
onSuccess: () => {
onClose();
},
onSettled: () => {
return queryClient.invalidateQueries(userPersonaOptions(roadmapId));
},
},
queryClient,
);
const roadmapTitle = roadmap?.json.title ?? '';
return (
<Modal
onClose={onClose}
wrapperClassName="max-w-[450px]"
bodyClassName="p-4"
>
<div className="mb-4 text-left">
<h2 className="text-lg font-semibold">Tell us more about yourself</h2>
<p className="mt-1 text-sm text-balance text-gray-500">
We'll use this information to help you get the best out of the AI
Tutor.
</p>
</div>
<UserPersonaForm
className="space-y-4"
roadmapTitle={roadmapTitle}
defaultValues={userPersona ?? undefined}
onSubmit={(data) => {
const trimmedGoal = data?.goal?.trim();
if (!trimmedGoal) {
toast.error('Please describe your goal');
return;
}
const trimmedCommit = data?.commit?.trim();
if (!trimmedCommit) {
toast.error(
'Please enter how many hours per week you can commit to learning',
);
return;
}
setUserPersona(data);
}}
isLoading={isSettingUserPersona}
/>
</Modal>
);
}

View File

@@ -0,0 +1,180 @@
import { useId, useRef, useState } from 'react';
import { SelectNative } from '../SelectNative';
import { Loader2Icon, MessageCircle } from 'lucide-react';
import { cn } from '../../lib/classname';
export type UserPersonaFormData = {
expertise: string;
goal: string;
commit: string;
};
type UserPersonaFormProps = {
roadmapTitle: string;
defaultValues?: UserPersonaFormData;
onSubmit: (data: UserPersonaFormData) => void;
className?: string;
isLoading?: boolean;
};
export function UserPersonaForm(props: UserPersonaFormProps) {
const {
roadmapTitle,
defaultValues,
className = '',
onSubmit,
isLoading,
} = props;
const [expertise, setExpertise] = useState(
defaultValues?.expertise ?? 'no-experience',
);
const [hasInitialGoal, setHasInitialGoal] = useState(!!defaultValues?.goal);
const [goal, setGoal] = useState(defaultValues?.goal ?? '');
const [commit, setCommit] = useState(defaultValues?.commit ?? '');
const expertiseFieldId = useId();
const goalFieldId = useId();
const commitFieldId = useId();
const goalRef = useRef<HTMLTextAreaElement>(null);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSubmit({ expertise, goal, commit });
};
const hasFormCompleted = !!expertise && !!goal && !!commit;
return (
<form onSubmit={handleSubmit} className={cn('space-y-8', className)}>
<div className="flex flex-col gap-3">
<label
className="text-sm font-medium text-gray-700"
htmlFor={expertiseFieldId}
>
Rate your expertise in {roadmapTitle}:
</label>
<SelectNative
id={expertiseFieldId}
value={expertise}
onChange={(e) => setExpertise(e.target.value)}
className="h-[40px] border-gray-300 text-sm focus:border-gray-500 focus:ring-1 focus:ring-gray-500"
>
<option value="" selected hidden>
Select your expertise
</option>
{[
'No experience (just starting out)',
'Beginner (less than 1 year of experience)',
'Intermediate (1-3 years of experience)',
'Expert (3-5 years of experience)',
'Master (5+ years of experience)',
].map((expertise) => (
<option key={expertise} value={expertise}>
{expertise}
</option>
))}
</SelectNative>
</div>
<div className="flex flex-col gap-3">
<label
className="text-sm font-medium text-gray-700"
htmlFor={goalFieldId}
>
What is your goal?{' '}
{hasInitialGoal &&
!defaultValues?.goal &&
`Tell us more about yourself`}
</label>
{!hasInitialGoal && (
<div className="flex flex-row flex-wrap gap-2">
{[
'Finding a job',
'Learning for fun',
'Building a side project',
'Switching careers',
'Getting a promotion',
'Filling knowledge gaps',
'Other (tell us more)',
].map((goalTemplate) => (
<button
key={goalTemplate}
className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-2 text-sm text-gray-600 transition-all hover:border-gray-300 hover:border-gray-400 hover:bg-gray-100"
onClick={() => {
if (goalTemplate !== 'Other (tell us more)') {
setGoal(`${goalTemplate}.`);
}
setHasInitialGoal(true);
setTimeout(() => {
goalRef.current?.focus();
}, 0);
}}
type="button"
>
{goalTemplate}
</button>
))}
</div>
)}
<textarea
ref={goalRef}
id={goalFieldId}
className={cn(
'block min-h-24 w-full resize-none rounded-lg border border-gray-300 bg-white px-4 py-3 text-sm outline-none placeholder:text-gray-400 focus:border-gray-500 focus:ring-1 focus:ring-gray-500',
!hasInitialGoal && 'hidden',
)}
placeholder={`e.g. need to find a job as soon as possible`}
value={goal}
onChange={(e) => setGoal(e.target.value)}
/>
</div>
<div className="flex flex-col gap-3">
<label
className="text-sm font-medium text-gray-700"
htmlFor={commitFieldId}
>
How many hours per week can you commit to learning?
</label>
<input
id={commitFieldId}
className="block h-[40px] w-full resize-none rounded-lg border border-gray-300 bg-white px-4 text-sm outline-none placeholder:text-gray-400 focus:border-gray-500 focus:ring-1 focus:ring-gray-500"
placeholder="e.g. 10 hours per week"
value={commit}
onChange={(e) => setCommit(e.target.value)}
/>
</div>
<button
disabled={isLoading || !hasFormCompleted}
type="submit"
className="mt-6 flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-black px-6 py-2 text-sm text-white transition-all hover:bg-gray-900 disabled:pointer-events-none disabled:opacity-50"
>
{isLoading ? (
<Loader2Icon className="size-4 animate-spin stroke-[2.5]" />
) : (
<>
{defaultValues ? (
<>
<MessageCircle className="size-4" />
Update Information
</>
) : (
<>
<MessageCircle className="size-4" />
Start Chatting
</>
)}
</>
)}
</button>
</form>
);
}

View File

@@ -11,7 +11,7 @@ export function ModalLoader(props: ModalLoaderProps) {
const { isLoading, text, error } = props;
return (
<div className="fixed left-0 right-0 top-0 z-100 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50">
<div className="fixed top-0 right-0 left-0 z-100 h-full items-center justify-center overflow-x-hidden overflow-y-auto overscroll-contain bg-black/50">
<div className="relative mx-auto flex h-full w-full items-center justify-center">
<div className="popup-body relative rounded-lg bg-white p-5 shadow-sm">
<div className="flex items-center">

View File

@@ -1,6 +1,6 @@
# Accessibility
Website accessibility is the practice of designing and developing websites that can be used by everyone, including people with disabilities. It involves implementing features and standards that make web content perceivable, operable, understandable, and robust for all users, regardless of their physical or cognitive abilities. This includes providing text alternatives for images, ensuring keyboard navigation, using sufficient color contrast, offering captions for audio content, and creating a consistent and predictable layout. Adhering to accessibility guidelines not only improves usability for people with disabilities but also enhances the overall user experience for all visitors while potentially increasing a site's reach and legal compliance.
Website accessibility means making sites usable by everyone, including those with disabilities. This involves clear design, like text for images, keyboard navigation, good color contrast, and captions. Following accessibility rules helps all users, not just those with disabilities, and can also help with legal compliance and reaching more people.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Angular
Angular is a popular open-source web application framework developed and maintained by Google. It uses TypeScript, a statically typed superset of JavaScript, to build scalable and efficient single-page applications (SPAs). Angular follows a component-based architecture, where the user interface is composed of reusable, self-contained components. The framework provides features like two-way data binding, dependency injection, and a powerful template syntax, which simplify the development of complex web applications. Angular also includes a comprehensive set of tools for testing, routing, and state management, making it a full-fledged solution for front-end development. Its modular structure and emphasis on best practices make it particularly suitable for large-scale enterprise applications.
Angular is a popular tool from Google for building websites and web apps. It uses TypeScript (a type of JavaScript) to create large, efficient single-page applications (SPAs), where content loads in one go without needing to reload the whole page. Angular builds UIs with reusable components, like building blocks. It has features like two-way data binding (data updates automatically in different places), dependency injection (helps manage code parts), and a strong template system. Angular also offers tools for testing, page navigation, and managing app data, making it great for big, complex projects.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Angular
Angular is a popular open-source web application framework developed and maintained by Google. It uses TypeScript, a statically typed superset of JavaScript, to build scalable and efficient single-page applications (SPAs). Angular follows a component-based architecture, where the user interface is composed of reusable, self-contained components. The framework provides features like two-way data binding, dependency injection, and a powerful template syntax, which simplify the development of complex web applications. Angular also includes a comprehensive set of tools for testing, routing, and state management, making it a full-fledged solution for front-end development. Its modular structure and emphasis on best practices make it particularly suitable for large-scale enterprise applications.
Angular is a popular tool from Google for building websites and web apps. It uses TypeScript (a type of JavaScript) to create large, efficient single-page applications (SPAs), where content loads in one go without needing to reload the whole page. Angular builds UIs with reusable components, like building blocks. It has features like two-way data binding (data updates automatically in different places), dependency injection (helps manage code parts), and a strong template system. Angular also offers tools for testing, page navigation, and managing app data, making it great for big, complex projects.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Apollo
Apollo GraphQL is a comprehensive platform for building and managing GraphQL-based data layers in modern applications. It provides a set of open-source tools and libraries that simplify the implementation of GraphQL on both the client and server sides. On the client side, Apollo Client offers powerful caching, state management, and data fetching capabilities, integrating seamlessly with various front-end frameworks. On the server side, Apollo Server facilitates the creation of GraphQL APIs, handling queries, mutations, and subscriptions efficiently. The Apollo platform also includes developer tools for schema management, performance monitoring, and API governance. By abstracting away much of the complexity of GraphQL implementation, Apollo enables developers to build faster, more scalable, and more maintainable applications with a unified data graph.
Apollo GraphQL is a platform for using GraphQL in apps. It has tools for both client (front-end) and server (back-end). Apollo Client helps with caching, data, and state in the front-end. Apollo Server helps build GraphQL APIs. Apollo also offers tools for managing your data graph, checking performance, and more. It simplifies GraphQL, helping build faster and more scalable apps.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Astro
Astro is a modern static site generator (SSG) and web framework designed for building fast, content-focused websites. It allows developers to use multiple frontend frameworks (like React, Vue, or Svelte) within the same project, automatically rendering components to static HTML at build time. Astro's unique "partial hydration" approach only sends JavaScript to the browser when necessary, resulting in significantly smaller bundle sizes and faster load times. The framework supports file-based routing, markdown content, and built-in optimizations for images and assets. Astro's component islands architecture enables developers to create interactive components while maintaining the performance benefits of static HTML, making it particularly well-suited for content-rich sites like blogs, documentation, and marketing pages.
Astro is a static site generator for fast, content-focused websites. It lets you use various frontend frameworks (like React, Vue, Svelte) and renders components to static HTML. Astro's "partial hydration" only sends JavaScript when needed, leading to smaller bundles and quicker loads. It supports file-based routing and markdown, making it great for blogs, docs, and marketing sites.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Astro
Astro is a modern static site generator (SSG) and web framework designed for building fast, content-focused websites. It allows developers to use multiple frontend frameworks (like React, Vue, or Svelte) within the same project, automatically rendering components to static HTML at build time. Astro's unique "partial hydration" approach only sends JavaScript to the browser when necessary, resulting in significantly smaller bundle sizes and faster load times. The framework supports file-based routing, markdown content, and built-in optimizations for images and assets. Astro's component islands architecture enables developers to create interactive components while maintaining the performance benefits of static HTML, making it particularly well-suited for content-rich sites like blogs, documentation, and marketing pages.
Astro is a static site generator for fast, content-focused websites. It lets you use various frontend frameworks (like React, Vue, Svelte) and renders components to static HTML. Astro's "partial hydration" only sends JavaScript when needed, leading to smaller bundles and quicker loads. It supports file-based routing and markdown, making it great for blogs, docs, and marketing sites.
Visit the following resources to learn more:

View File

@@ -1,15 +1,6 @@
# Authentication Strategies
Authentication strategies are methods or techniques used to verify the identity of a user or system in order to grant access to a protected resource. There are several different authentication strategies that can be used, including:
- Basic Authentication
- Session Based Authentication
- Token Based Authentication
- JWT Authentication
- OAuth
- SSO
You don't necessarily need to learn all of these, how to implement and the ins and outs from the get go. But it's important to know what they are and how they work. This will help you make better decisions when choosing an authentication strategy for your application.
Authentication strategies verify a user's identity to grant access. Common methods include Basic Auth (username/password), Session-based (server remembers login), Token-based (like JWT, a secure digital key), OAuth (for third-party access like "Login with Google"), and SSO (Single Sign-On, one login for many apps). Knowing these helps choose the right security for your app.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# BEM
The Block, Element, Modifier methodology (commonly referred to as BEM) is a popular naming convention for classes in HTML and CSS. Developed by the team at Yandex, its goal is to help developers better understand the relationship between the HTML and CSS in a given project.
BEM (Block, Element, Modifier) is a way to name CSS classes in HTML. It helps developers see how HTML and CSS relate in a project. The goal is to make CSS more modular, reusable, and easier to understand, especially in large projects, by creating clear, consistent naming rules.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# BitBucket
Bitbucket is a web-based version control repository hosting service owned by Atlassian. It provides Git and Mercurial version control systems for both open source and private projects. Bitbucket offers features such as pull requests, branch permissions, and in-line commenting for code review. It integrates seamlessly with other Atlassian products like Jira and Trello, facilitating project management and issue tracking. Bitbucket provides both cloud-hosted and self-hosted options, catering to different organizational needs. It supports continuous integration and deployment (CI/CD) through Bitbucket Pipelines.
Bitbucket, by Atlassian, hosts Git and Mercurial code projects. Its for both open-source and private work. Bitbucket has features like pull requests and code review comments. It works well with other Atlassian tools like Jira for project management. You can use it online or host it yourself. Bitbucket also supports CI/CD (automating build and release) with Bitbucket Pipelines.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Browsers
A web browser is a software application that enables a user to access and display web pages or other online content through its graphical user interface.
Web browsers are apps that let you view websites. When you type a web address, the browser asks a server for the page. It then reads the HTML (structure), CSS (style), and JavaScript (interactivity) to show you the webpage. Browsers have rendering engines (like Blink in Chrome or Gecko in Firefox) to display content and JavaScript engines (like V8 in Chrome) to run code. They also handle things like security, bookmarks, and history.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Build Tools
Build tools are software utilities designed to automate the process of creating executable applications from source code. They handle tasks such as compiling, linking, minifying, and bundling code, as well as running tests and managing dependencies. Common build tools include Make, Gradle, Maven, Webpack, and Gulp. These tools streamline development workflows by reducing manual steps, ensuring consistency across different environments, and optimizing output for production. They often support features like incremental builds, parallel processing, and custom task definitions. Build tools are crucial in modern software development, especially for large-scale projects, as they improve efficiency, reduce errors, and facilitate continuous integration and deployment processes.
Build tools automate making apps from source code. They compile, link, minify (shrink), and bundle code, run tests, and manage dependencies. Examples are Webpack, Vite, and Parcel for web development. Build tools speed up development, ensure consistency, and optimize code for users. They're key for modern software, especially big projects, making work more efficient and enabling continuous integration (CI).
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Content Security Policy
Content Security Policy (CSP) is a security standard implemented by web browsers to prevent cross-site scripting (XSS), clickjacking, and other code injection attacks. It works by allowing web developers to specify which sources of content are trusted and can be loaded on a web page. CSP is typically implemented through HTTP headers or meta tags, defining rules for various types of resources like scripts, stylesheets, images, and fonts. By restricting the origins from which content can be loaded, CSP significantly reduces the risk of malicious code execution. It also provides features like reporting violations to help developers identify and fix potential security issues. While powerful, implementing CSP requires careful configuration to balance security with functionality, especially for sites using third-party resources or inline scripts.
Content Security Policy (CSP) is a security feature in web browsers that helps stop attacks like cross-site scripting (XSS) and clickjacking. It lets website creators tell the browser which sources of content (like scripts, styles, and images) are safe to load. This is done using special instructions sent with the webpage. By limiting where content can come from, CSP makes it much harder for bad code to run on a site. It can also report problems, helping developers find and fix security holes. Setting up CSP needs care to make sure everything works right, especially if the site uses content from other places.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# CORS
Cross-Origin Resource Sharing (CORS) is a security mechanism implemented by web browsers to control access to resources (like APIs or fonts) on a web page from a different domain than the one serving the web page. It extends and adds flexibility to the Same-Origin Policy, allowing servers to specify who can access their resources. CORS works through a system of HTTP headers, where browsers send a preflight request to the server hosting the cross-origin resource, and the server responds with headers indicating whether the actual request is allowed. This mechanism helps prevent unauthorized access to sensitive data while enabling legitimate cross-origin requests. CORS is crucial for modern web applications that often integrate services and resources from multiple domains, balancing security needs with the functionality requirements of complex, distributed web systems.
Cross-Origin Resource Sharing (CORS) is a browser security feature that controls how web pages access resources from different domains. It allows servers to specify who can access their assets (like APIs or fonts). CORS uses HTTP headers; browsers may send a preflight request to check if the actual request is allowed. This prevents unauthorized access while enabling legitimate cross-origin requests, vital for modern apps using multiple domain services.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Credentials API
The Credential Management API is a web standard that allows websites to interact with the browser's credential manager to store, retrieve, and manage user credentials. It provides a programmatic interface for seamless and secure user authentication, enabling features like automatic sign-in and one-tap sign-up. The API supports various credential types, including passwords, federated identities, and public key credentials. By leveraging this API, developers can improve user experience by reducing login friction, implementing smoother account switching, and enhancing overall security. It works in conjunction with password managers and platform authenticators, helping to streamline authentication processes across devices and browsers while adhering to modern security practices.
The Credential Management API helps websites work with the browser's password manager. It lets sites store and get user logins securely, making sign-in and sign-up easier (like auto sign-in). It supports passwords and other login types. This API improves login experience and security by working with password managers and platform authenticators, making logins smoother across devices.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# CSS Architecture
CSS architecture refers to the methodologies and organizational strategies used to structure and maintain CSS code in large-scale web projects. It focuses on creating scalable, maintainable, and modular stylesheets to manage the growing complexity of web applications. Key concepts include naming conventions (like BEM or SMACSS), component-based design, separation of concerns, and the use of preprocessors (such as Sass or Less). CSS architecture often employs techniques like CSS modules, utility classes, or CSS-in-JS solutions to improve code reusability and reduce specificity conflicts. The goal is to create a systematic approach to styling that enhances collaboration among developers, reduces code duplication, and facilitates easier updates and maintenance of the visual design across an entire application or website.
CSS architecture organizes CSS in large web projects for scalability and maintenance. It involves naming conventions (like BEM), component-based design, and tools like preprocessors. Techniques like CSS modules or utility classes enhance reusability and reduce conflicts. The aim is a systematic styling approach for better collaboration, less code duplication, and easier updates.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# CSS Preprocessors
CSS preprocessors are scripting languages that extend the capabilities of standard CSS, allowing developers to write more maintainable and efficient stylesheets. They introduce features like variables, nesting, mixins, functions, and mathematical operations, which are then compiled into standard CSS. Popular preprocessors include Sass, Less, and Stylus. These tools enable developers to organize styles more logically, reuse code more effectively, and create complex CSS structures with less repetition. Preprocessors often support features like partials for modular stylesheets and built-in color manipulation functions. By using a preprocessor, developers can write more DRY (Don't Repeat Yourself) code, manage large-scale projects more easily, and potentially improve the performance of their stylesheets through optimization during the compilation process.
CSS preprocessors (like Sass, Less, Stylus) add extra features to CSS, making it easier to write and manage. They offer things like variables (for colors, sizes), nesting (to organize styles), mixins (reusable style blocks), and functions. This helps keep CSS tidy and avoid repetition, especially in big projects. The preprocessor code is then turned into regular CSS that browsers understand. They make CSS more powerful and efficient.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# CSS
CSS (Cascading Style Sheets) is a styling language used to describe the presentation of a document written in HTML or XML. It defines how elements should be displayed on screen, on paper, or in other media. CSS separates the design from the content, allowing for greater flexibility and control over the layout, colors, and fonts of web pages. It uses a system of selectors to target HTML elements and apply styles to them. CSS supports responsive design through media queries, enabling the creation of layouts that adapt to different screen sizes and devices. The cascade, inheritance, and specificity are key concepts in CSS that determine how styles are applied when multiple rules target the same element. Modern CSS includes features like Flexbox and Grid for advanced layout control, animations, and transitions for creating dynamic user interfaces.
CSS (Cascading Style Sheets) is what makes websites look good. It's a language used to style HTML documents, controlling things like layout, colors, and fonts. CSS keeps the design separate from the HTML content, which makes websites easier to manage. It uses "selectors" to pick HTML elements and apply styles. CSS also helps make websites responsive, meaning they look good on any device, thanks to media queries. Important ideas in CSS are the cascade (how styles override each other), inheritance (how styles pass from parent to child elements), and specificity (which style rule wins). Modern CSS has cool tools like Flexbox and Grid for layout, plus animations and transitions for interactive designs.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Custom Elements
One of the key features of the Web Components standard is the ability to create custom elements that encapsulate your functionality on an HTML page, rather than having to make do with a long, nested batch of elements that together provide a custom page feature.
Custom Elements are a part of Web Components that let you create your own HTML tags. This means you can make reusable parts for your webpage that have their own special behavior, instead of using lots of nested standard HTML tags. It helps keep your HTML cleaner and your components easier to manage.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Cypress
Cypress framework is a JavaScript-based end-to-end testing framework built on top of Mocha a feature-rich JavaScript test framework running on and in the browser, making asynchronous testing simple and convenient. It also uses a BDD/TDD assertion library and a browser to pair with any JavaScript testing framework.
Cypress is a tool for testing websites from start to finish, just like a user would. It's written in JavaScript and based on Mocha (another JavaScript testing tool). Cypress runs tests directly in the browser, which makes testing things that happen over time (asynchronous testing) easier. It also uses common testing approaches like BDD/TDD and can work with other JavaScript testing tools.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Desktop Applications in JavaScript
Desktop applications applications typically use frameworks like Electron, NW.js (Node-WebKit), or Tauri, which combine a JavaScript runtime with a native GUI toolkit. This approach allows developers to use their web development skills to create cross-platform desktop apps. Electron, developed by GitHub, is particularly popular, powering applications like Visual Studio Code, Atom, and Discord. These frameworks provide APIs to access native system features, enabling JavaScript to interact with the file system, system tray, and other OS-specific functionalities. While offering rapid development and cross-platform compatibility, JavaScript desktop apps can face challenges in terms of performance and resource usage compared to traditional native applications. However, they benefit from the vast ecosystem of JavaScript libraries and tools, making them an attractive option for many developers and businesses.
JavaScript can build desktop apps using tools like Electron, NW.js, or Tauri. These mix JavaScript with a native look and feel, letting web developers make desktop apps for different systems (like Windows, Mac, Linux). Electron is very popular (used by VS Code, Discord). These tools let JavaScript access computer features like files. While they make development fast and work on many systems, they can sometimes be slower or use more resources than fully native apps. But, they benefit from all the JavaScript tools available.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Device Orientation API
The Device Orientation API is a web API that provides access to the device's orientation and motion data, such as its pitch, roll, and yaw. It allows web developers to build applications that can respond to the device's orientation and motion, such as augmented reality and motion-controlled games. To use the Device Orientation API, a web page must first request permission from the user to access the device's orientation data. If permission is granted, the page can then use the DeviceOrientationEvent object to access the device's orientation data and respond to changes in orientation. The API provides several properties for accessing the device's orientation and motion data, as well as events for detecting changes in orientation.
The Device Orientation API lets websites know how a device is tilted or moving (like its pitch, roll, and yaw). This is for apps that react to movement, like augmented reality or motion games. The site must ask for permission first. If allowed, it can get the device's orientation and react to changes. It helps make interactive, motion-aware web apps.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# DNS
DNS (Domain Name System) is a hierarchical, decentralized naming system for computers, services, or other resources connected to the Internet or a private network. It translates human-readable domain names (like www.example.com) into IP addresses (like 192.0.2.1) that computers use to identify each other. DNS servers distributed worldwide work together to resolve these queries, forming a global directory service. The system uses a tree-like structure with root servers at the top, followed by top-level domain servers (.com, .org, etc.), authoritative name servers for specific domains, and local DNS servers. DNS is crucial for the functioning of the Internet, enabling users to access websites and services using memorable names instead of numerical IP addresses.
DNS (Domain Name System) translates human-readable domain names (e.g., www.example.com) into IP addresses computers use. It's a global, decentralized system of servers. When you enter a domain name, DNS servers find the corresponding IP address, letting your browser connect to the website. This makes navigating the internet easy, as you don't need to remember numeric IP addresses.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Electron
Electron is an open-source framework developed by GitHub that enables developers to build cross-platform desktop applications using web technologies. It combines the Chromium rendering engine with the Node.js runtime, allowing applications to be written in HTML, CSS, and JavaScript. Electron provides APIs to access native operating system functions, bridging the gap between web and desktop development. It's widely used for creating popular applications like Visual Studio Code, Atom, and Discord. Electron apps benefit from rapid development cycles, cross-platform compatibility, and access to a vast ecosystem of web technologies and Node.js modules. However, they can face challenges with resource usage and performance compared to native applications. Despite these trade-offs, Electron remains a popular choice for developers seeking to leverage web skills for desktop app development.
Electron is a framework for building cross-platform desktop apps with web tech (HTML, CSS, JavaScript). It uses Chromium and Node.js, allowing access to native OS functions. Popular apps like VS Code use Electron. It enables fast development and cross-platform use, but can be resource-intensive. Still, it's a go-to for web developers creating desktop apps.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Eleventy
Eleventy (11ty) is a simple to use, easy to customize, highly performant and powerful static site generator with a helpful set of plugins (e.g. navigation, build-time image transformations, cache assets). Pages can be built and written with a variety of template languages (HTML, Markdown, JavaScript, Liquid, Nunjucks, Handlebars, Mustache, EJS, Haml, Pug or JS template literals). But it also offers the possibility to dynamically create pages from local data or external sources that are compiled at build time. It has zero client-side JavaScript dependencies.
Eleventy (or 11ty) is a tool for building fast websites that don't change often (static sites). It's easy to use and change to fit your needs. You can write pages using many different template languages like HTML, Markdown, or JavaScript. Eleventy can also create pages from data you have or from other websites when you build your site. A big plus is that it doesn't add any extra JavaScript to the user's browser, making sites load quickly. It also has helpful plugins for things like navigation or changing images.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# esbuild
esbuild is a high-performance JavaScript bundler and minifier designed for speed and efficiency. Created by Evan Wallace, it's written in Go and compiles to native code, making it significantly faster than traditional JavaScript-based build tools. esbuild supports modern JavaScript features, TypeScript, and JSX out of the box, with near-instant bundling times even for large projects. It offers a simple API and command-line interface, making it easy to integrate into existing build pipelines. While primarily focused on speed, esbuild also provides basic code splitting, tree shaking, and source map generation. Its extreme performance makes it particularly suitable for development environments and as a foundation for other build tools, though it may lack some advanced features found in more mature bundlers.
esbuild is a very fast JavaScript bundler and minifier. It's written in Go, so it's much quicker than older tools. esbuild handles modern JavaScript, TypeScript, and JSX. It bundles code almost instantly, even for big projects. It's easy to use with a simple API. While super fast, it might not have all the advanced features of older bundlers, but it's great for development and as a base for other tools.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# ESLint
ESLint is a popular open-source static code analysis tool for identifying and fixing problems in JavaScript code. It enforces coding standards, detects potential errors, and promotes consistent coding practices across projects. ESLint is highly configurable, allowing developers to define custom rules or use preset configurations. It supports modern JavaScript features, JSX, and TypeScript through plugins. ESLint can be integrated into development workflows through IDE extensions, build processes, or git hooks, providing real-time feedback to developers. Its ability to automatically fix many issues it detects makes it a valuable tool for maintaining code quality and consistency, especially in large teams or projects. ESLint's extensibility and wide adoption in the JavaScript ecosystem have made it a standard tool in modern JavaScript development.
ESLint is a tool that checks JavaScript code for problems. It helps keep code style consistent and finds errors. ESLint is very flexible; you can set your own rules or use ready-made ones. It works with modern JavaScript, JSX, and TypeScript. You can use ESLint in your code editor or when you build your project to get live feedback. It can even fix many issues automatically, which is great for team projects.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Fetch API
The Fetch API is a modern JavaScript interface for making HTTP requests in web browsers. It provides a more powerful and flexible way to send and receive data compared to older methods like XMLHttpRequest. Fetch uses Promises, allowing for cleaner asynchronous code. It supports various data formats, custom headers, and different types of requests (GET, POST, etc.). The API is designed to be extensible and integrates well with other web technologies. While simpler for basic use cases, Fetch also handles complex scenarios like request cancellation and reading streamed responses. It's widely supported in modern browsers and has become the standard for network requests in client-side JavaScript applications.
The Fetch API is a new way for JavaScript in browsers to make HTTP requests (getting or sending data online). It's better and more flexible than older ways like XMLHttpRequest. Fetch uses Promises, making code that waits for data cleaner. It handles different data types and request methods (GET, POST). It's now the standard way for websites to talk to servers and is supported by most browsers.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Flutter
Flutter is an open-source UI software development kit created by Google for building natively compiled, multi-platform applications from a single codebase. It uses the Dart programming language and provides a rich set of pre-designed widgets for creating responsive and visually appealing user interfaces. Flutter's architecture allows for fast development with hot reload, enabling developers to see changes instantly. It supports iOS, Android, web, and desktop platforms, offering true cross-platform development. Flutter uses a custom rendering engine, Skia, to draw UI components, ensuring consistent appearance across devices. While known for mobile app development, Flutter's expanding ecosystem and performance improvements have increased its adoption for web and desktop applications as well.
Flutter, by Google, is a tool for building apps for many platforms (iOS, Android, web, desktop) from one set of code. It uses the Dart language and has many ready-made UI parts (widgets) for making good-looking apps. Flutter is fast to develop with because of its hot reload (see changes instantly). It draws its own UI, so apps look the same everywhere. It's very popular for mobile apps and growing for web and desktop too.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Flutter
Flutter is an open-source UI software development kit created by Google for building natively compiled, multi-platform applications from a single codebase. It uses the Dart programming language and provides a rich set of pre-designed widgets for creating responsive and visually appealing user interfaces. Flutter's architecture allows for fast development with hot reload, enabling developers to see changes instantly. It supports iOS, Android, web, and desktop platforms, offering true cross-platform development. Flutter uses a custom rendering engine, Skia, to draw UI components, ensuring consistent appearance across devices. While known for mobile app development, Flutter's expanding ecosystem and performance improvements have increased its adoption for web and desktop applications as well.
Flutter, by Google, is a tool for building apps for many platforms (iOS, Android, web, desktop) from one set of code. It uses the Dart language and has many ready-made UI parts (widgets) for making good-looking apps. Flutter is fast to develop with because of its hot reload (see changes instantly). It draws its own UI, so apps look the same everywhere. It's very popular for mobile apps and growing for web and desktop too.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Forms and Validations
Before submitting data to the server, it is important to ensure all required form controls are filled out, in the correct format. This is called client-side form validation, and helps ensure data submitted matches the requirements set forth in the various form controls.
Before sending data from a form to a server, it's important to check if all required fields are filled in correctly. This is called client-side form validation. It helps make sure the data sent matches what the form expects, improving data quality and user experience.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Git
Git is a distributed version control system designed to handle projects of any size with speed and efficiency. Created by Linus Torvalds in 2005, Git tracks changes in source code during software development, allowing multiple developers to work together on non-linear development. It provides strong support for branching, merging, and distributed development workflows. Git maintains a complete history of all changes, enabling easy rollbacks and comparisons between versions. Its distributed nature means each developer has a full copy of the repository, allowing for offline work and backup. Git's speed, flexibility, and robust branching and merging capabilities have made it the most widely used version control system in software development, particularly for open-source projects.
Git is a tool for tracking code changes in software projects. It lets many developers work together by keeping a history of all changes. Git is great for branching (working on different features at once) and merging (combining changes). Everyone has a full copy of the project, so they can work offline. Git is fast, flexible, and the most popular version control system, especially for open-source projects.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# GitHub
GitHub has become a central hub for open-source projects and is widely used by developers, companies, and organizations for both private and public repositories. It was acquired by Microsoft in 2018 but continues to operate as a relatively independent entity. GitHub's popularity has made it an essential tool in modern software development workflows and a key platform for showcasing coding projects and contributing to open-source software.
GitHub is a popular website for hosting Git projects. It's a key place for open-source software and is used by developers and companies for both public and private code. Microsoft bought GitHub in 2018. It's a vital tool for modern software development, showing off projects, and contributing to open-source.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# GitLab
GitLab is a web-based DevOps platform that provides a complete solution for the software development lifecycle. GitLab emphasizes an all-in-one approach, integrating various development tools into a single platform. It's available as both a cloud-hosted service and a self-hosted solution, giving organizations flexibility in deployment. GitLab's focus on DevOps practices and its comprehensive feature set make it popular among enterprises and teams seeking a unified platform for their entire development workflow. While similar to GitHub in many respects, GitLab's integrated CI/CD capabilities and self-hosting options are often cited as key differentiators.
GitLab is a web platform for the entire software development process (DevOps). It offers many tools in one place. You can use it online or host it yourself. GitLab is popular with businesses that want one platform for all their development work. It's like GitHub but often highlighted for its built-in CI/CD (automating build and release) and self-hosting options.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Graphql
GraphQL is a query language and runtime for APIs, developed by Facebook. GraphQL's flexibility and efficiency make it popular for building complex applications, especially those with diverse client requirements. It's particularly useful for mobile applications where bandwidth efficiency is crucial. While it requires a paradigm shift from REST, many developers and organizations find GraphQL's benefits outweigh the learning curve, especially for large-scale or rapidly evolving APIs.
GraphQL, by Facebook, is a way to get data for apps. Unlike older methods, it lets apps ask for exactly the data they need, no more, no less. This is great for mobile apps where saving data is important. It's different from REST but good for big or changing APIs.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# How Does The Internet Work
The internet is a global network that connects computers and devices so they can share information with each other. Its how you browse websites, send emails, watch videos, and use apps. Think of it like a giant web that links everything together.
The internet is a global network connecting computers and devices for information sharing, enabling activities like browsing websites, sending emails, and streaming videos. It acts as a vast web linking everything, facilitating communication and access to online resources and services worldwide.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# HTML Templates
The `<template>` HTML element is a mechanism for holding HTML that is not to be rendered immediately when a page is loaded but may be instantiated subsequently during runtime using JavaScript. Think of a template as a content fragment that is being stored for subsequent use in the document.
The `<template>` HTML tag holds HTML content that isn't shown right away when a page loads. Instead, JavaScript can use it later to create new elements on the page. It's like a blueprint for HTML parts you want to reuse.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# HTML
HTML (Hypertext Markup Language) is the standard markup language used to create web pages and web applications. It provides a structure for content on the World Wide Web, using a system of elements and attributes to define the layout and content of a document. HTML elements are represented by tags, which browsers interpret to render the visual and auditory elements of a web page. The language has evolved through several versions, with HTML5 being the current standard, introducing semantic elements, improved multimedia support, and enhanced form controls. HTML works in conjunction with CSS for styling and JavaScript for interactivity, forming the foundation of modern web development.
HTML (Hypertext Markup Language) is the standard for creating web pages, structuring content with elements and attributes. Browsers interpret HTML tags to render pages. HTML5, the current standard, adds semantic elements, multimedia support, and form controls. It works with CSS for styling and JavaScript for interactivity, forming web development's foundation.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# HTTPS
Hypertext transfer protocol secure (HTTPS) is the secure version of HTTP, which is the primary protocol used to send data between a web browser and a website. HTTPS is encrypted in order to increase security of data transfer. This is particularly important when users transmit sensitive data, such as by logging into a bank account, email service, or health insurance provider.
HTTPS (Hypertext Transfer Protocol Secure) is the secure version of HTTP, the main way data is sent between your browser and websites. HTTPS encrypts this data, making it safer. This is very important for sensitive information like bank logins or emails. It keeps your data private and secure online.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Internet
The Internet is a global network of interconnected computer networks that use the Internet Protocol Suite (TCP/IP) to communicate. It enables the exchange of data, information, and services across the world, connecting billions of devices and users. The Internet has revolutionized communication, commerce, education, and entertainment, becoming an integral part of modern society. It supports various applications and services, from web browsing and instant messaging to streaming media and online gaming. While offering unprecedented access to information and connectivity, the Internet also raises concerns about privacy, security, and digital divide issues.
The Internet, a global network of interconnected computer networks using TCP/IP, facilitates worldwide data exchange, connecting billions of devices. It has revolutionized communication, commerce, education, and entertainment, supporting diverse applications from web browsing to streaming. While offering vast information access, it also presents privacy, security, and digital divide challenges.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Ionic
Ionic is an open-source UI toolkit for building high-quality, cross-platform mobile and desktop applications using web technologies (HTML, CSS, and JavaScript). It leverages popular web frameworks like Angular, React, or Vue.js for application logic, while providing a rich set of pre-built UI components and native device functionalities. Ionic uses Cordova or Capacitor to wrap web apps for native deployment, allowing developers to create hybrid apps that can access device features through plugins. The framework emphasizes performance and native look-and-feel, offering adaptive styling for different platforms. With its focus on web standards and cross-platform compatibility, Ionic enables developers to maintain a single codebase for multiple platforms, making it a popular choice for rapid mobile app development.
Ionic is a tool for building mobile and desktop apps using web tech (HTML, CSS, JavaScript) and frameworks like Angular, React, or Vue. It gives you ready-made UI parts and access to phone features. Ionic wraps your web app so it can be a native app. It tries to make apps look and feel native on different platforms. Ionic is good for quickly making apps for many platforms with one codebase.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# JavaScript
JavaScript is a high-level, interpreted programming language that is a core technology of the World Wide Web. It allows for dynamic, client-side scripting in web browsers, enabling interactive web pages and user interfaces. JavaScript supports object-oriented, imperative, and functional programming styles. It's also used server-side through Node.js, for desktop application development with frameworks like Electron, and in various other contexts. The language features dynamic typing, first-class functions, and prototype-based object-orientation. JavaScript's ubiquity in web development, coupled with its versatility and continuous evolution through ECMAScript standards, has made it one of the most popular programming languages in use today.
JavaScript is a key programming language for the web. It makes websites interactive, like when things move or change when you click them. It works in web browsers but also on servers (with Node.js) and for desktop apps. JavaScript is flexible and always updating, making it very popular for all kinds of web development.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Jest
Jest is a popular JavaScript testing framework developed by Facebook. It provides a comprehensive solution for unit testing JavaScript code, with a focus on simplicity and minimal configuration. Jest offers features such as automatic mocking, code coverage reporting, and snapshot testing. It supports testing of both synchronous and asynchronous code, and can be used with various JavaScript frameworks and libraries, including React, Angular, and Vue. Jest's built-in assertion library and test runner make it easy to write and execute tests quickly. Its ability to run tests in parallel and its intelligent test-watching mode contribute to fast test execution, making it a preferred choice for many developers and organizations in the JavaScript ecosystem.
Jest is a popular JavaScript testing tool from Facebook. It's made for easy unit testing (testing small code parts). Jest has features like auto mocking, code coverage reports, and snapshot testing. It works with React, Angular, and Vue. Jest has its own tools for writing and running tests fast. It can run tests at the same time and watches for changes, making it a top choice for JavaScript developers.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# DOM Manipulation
The Document Object Model (DOM) is a programming interface built for HTML and XML documents. It represents the page that allows programs and scripts to dynamically update the document structure, content, and style. With DOM, we can easily access and manipulate tags, IDs, classes, attributes, etc.
The Document Object Model (DOM) is how programs see HTML and XML documents. It lets scripts change a page's structure, content, and style dynamically. With the DOM, you can easily work with HTML tags, IDs, classes, and attributes to make webpages interactive.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# HTML Basics
HTML stands for HyperText Markup Language. It is used on the frontend and gives the structure to the webpage which you can style using CSS and make interactive using JavaScript.
HTML (HyperText Markup Language) is the backbone of webpages. It structures the content you see online. You use CSS to style this HTML structure and JavaScript to make it interactive. Think of HTML as the skeleton of a website.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# CSS Basics
CSS or Cascading Style Sheets is the language used to style the frontend of any website. CSS is a cornerstone technology of the World Wide Web, alongside HTML and JavaScript.
CSS (Cascading Style Sheets) is the language for styling websites. It makes the frontend look good. Along with HTML and JavaScript, CSS is a key part of the World Wide Web. It controls colors, fonts, layout, and more to visually design webpages.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# JavaScript
JavaScript allows you to add interactivity to your pages. Common examples that you may have seen on the websites are sliders, click interactions, popups and so on.
JavaScript makes webpages interactive. Think of sliders, what happens when you click something, or pop-up messages that's often JavaScript at work. It adds dynamic behavior to the static structure created by HTML and styled by CSS.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Linters formatters
Linters and formatters are tools used in software development to improve code quality and consistency. Linters analyze source code to detect programming errors, bugs, stylistic issues, and suspicious constructs, often enforcing a set of predefined or custom rules. Formatters automatically restructure code to adhere to a consistent style, adjusting elements like indentation, line breaks, and spacing. Together, these tools help maintain code standards across projects and teams, enhance readability, catch potential errors early, and reduce the cognitive load on developers during code reviews. Popular examples include ESLint for JavaScript linting and Prettier for code formatting, both of which can be integrated into development workflows and IDEs for real-time feedback and automatic corrections.
Linters and formatters boost code quality. Linters find errors, bugs, and style issues by checking code against rules. Formatters automatically fix style, like indents and spacing. They help keep code consistent, readable, and catch errors early. ESLint and Prettier are popular examples, often used in code editors for live feedback.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Location API
The Geolocation API is a web API that provides access to the device's location data, such as latitude and longitude. It allows web developers to build location-based applications, such as mapping and location sharing, by using the device's GPS, Wi-Fi, and other sensors to determine the user's location. To use the Geolocation API, a web page must first request permission from the user to access their location. If permission is granted, the page can then use the `navigator.geolocation` object to access the device's location data. The API provides several methods for getting the user's current location, watching for location changes, and calculating distances between two locations.
The Geolocation API lets websites know a device's location (like latitude and longitude). This is for apps that use location, like maps. It uses GPS, Wi-Fi, and other sensors. The website must ask for permission first. If given, it can get the current location, watch for changes, or find distances. This helps make location-aware web apps.
Visit the following resources to learn more:

View File

@@ -1,16 +1,6 @@
# Making layouts
Making layouts in web design involves organizing content and visual elements on a page to create an effective and aesthetically pleasing user interface. Modern layout techniques primarily use CSS, with key approaches including:
1. Flexbox for one-dimensional layouts (rows or columns)
2. CSS Grid for two-dimensional layouts
3. Responsive design principles for adaptability across devices
4. CSS frameworks like Bootstrap or Tailwind for rapid development
5. Custom CSS properties (variables) for consistent styling
6. Media queries for device-specific adjustments
7. CSS positioning and float for specific element placement
These tools allow designers to create complex, responsive layouts that maintain consistency and usability across various screen sizes and devices. Effective layouts consider visual hierarchy, user flow, accessibility, and content prioritization to enhance the overall user experience and achieve design goals.
Making web layouts means arranging content on a page well. Modern CSS uses Flexbox (for rows/columns) and CSS Grid (for 2D layouts). Responsive design makes sites fit all devices. Frameworks like Bootstrap or Tailwind help build faster. Good layouts think about what's important, how users move through the page, and if everyone can use it. This makes the site look good and work well.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Mobile applications
Mobile applications are software programs designed to run on mobile devices such as smartphones and tablets. They are typically distributed through app stores like Google Play or Apple's App Store. Mobile apps can be native (built specifically for one platform using languages like Swift for iOS or Kotlin for Android), hybrid (web technologies wrapped in a native container), or cross-platform (using frameworks like React Native or Flutter). These apps leverage device-specific features such as GPS, cameras, and sensors to provide rich, interactive experiences. They cover a wide range of functions from productivity and entertainment to social networking and e-commerce. Mobile app development considers factors like user interface design, performance optimization, offline functionality, and security to ensure a smooth user experience across various devices and operating systems.
Mobile apps are programs for phones and tablets, usually from app stores. They can be native (for one OS like iOS or Android), hybrid (web tech in a native shell), or cross-platform (like React Native). Apps use phone features like GPS and cameras. They do many things from games to shopping. Good mobile apps focus on easy use, speed, offline working, and security.
- [@official@React Native](https://reactnative.dev/)
- [@official@Flutter](https://flutter.dev)

View File

@@ -1,6 +1,6 @@
# Module Bundlers
Module bundlers are development tools that combine multiple JavaScript files and their dependencies into a single file, optimized for web browsers. They resolve and manage dependencies, transform and optimize code, and often support features like code splitting and lazy loading. Popular module bundlers include Webpack, Rollup, and Parcel. These tools address challenges in managing complex JavaScript applications by organizing code into modules, eliminating global scope pollution, and improving load times. Bundlers typically support various file formats, enable the use of modern JavaScript features through transpilation, and integrate with task runners and other build tools. Their primary goal is to streamline the development process and enhance application performance in production environments.
Module bundlers are tools that take many JavaScript files and combine them into one, which is better for web browsers. They handle dependencies, improve code, and can split code for faster loading. Webpack, Rollup, and Parcel are examples. They help manage big JavaScript projects by organizing code into modules and making load times better. Bundlers also let you use new JavaScript features by changing them to older versions if needed. They make development smoother and apps run better.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Next.js
Next.js is a React-based open-source framework for building server-side rendered and statically generated web applications. It provides features like automatic code splitting, optimized performance, and simplified routing out of the box. Next.js supports both static site generation (SSG) and server-side rendering (SSR), allowing developers to choose the most appropriate rendering method for each page. The framework offers built-in CSS support, API routes for backend functionality, and easy deployment options. Next.js is known for its developer-friendly experience, with features like hot module replacement and automatic prefetching. Its ability to create hybrid apps that combine static and server-rendered pages makes it popular for building scalable, SEO-friendly web applications.
Next.js is a React framework for building websites that can be server-rendered (built on the server) or static (built beforehand). It offers features like auto code splitting, fast performance, and easy page routing. You can choose how each page is made. Next.js has CSS support, API routes (for backend tasks), and simple deployment. It's known for being developer-friendly, with live updates and smart preloading. It's great for big, SEO-friendly sites.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Next.js
Next.js is a React-based open-source framework for building server-side rendered and statically generated web applications. It provides features like automatic code splitting, optimized performance, and simplified routing out of the box. Next.js supports both static site generation (SSG) and server-side rendering (SSR), allowing developers to choose the most appropriate rendering method for each page. The framework offers built-in CSS support, API routes for backend functionality, and easy deployment options. Next.js is known for its developer-friendly experience, with features like hot module replacement and automatic prefetching. Its ability to create hybrid apps that combine static and server-rendered pages makes it popular for building scalable, SEO-friendly web applications.
Next.js is a React framework for building websites that can be server-rendered (built on the server) or static (built beforehand). It offers features like auto code splitting, fast performance, and easy page routing. You can choose how each page is made. Next.js has CSS support, API routes (for backend tasks), and simple deployment. It's known for being developer-friendly, with live updates and smart preloading. It's great for big, SEO-friendly sites.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Notifications API
The Notifications API is a web API that allows web pages to display system-level notifications to the user. These notifications can be used to alert the user of important events, such as new messages or updates, even when the web page is not open in the browser. To use the Notifications API, a web page must first request permission from the user to display notifications. If permission is granted, the page can then use the `Notification` constructor to create a new notification and display it to the user. The notification can include a title, body text, and an icon, and it can be customized with options such as a timeout and a click action.
The Notifications API lets websites show system alerts to users, like for new messages or updates, even if the site isn't open. The site must ask for permission first. If allowed, it can create notifications with a title, text, and icon. These can also have a timeout or an action when clicked. It helps keep users informed about important events from the website.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# npm
npm (Node Package Manager) is the default package manager for Node.js, providing a vast ecosystem of reusable JavaScript code. It allows developers to easily share, discover, and install packages (libraries and tools) for their projects. npm consists of a command-line interface for package installation and management, and an online repository of open-source packages. It handles dependency management, version control, and script running for Node.js projects. The npm registry is the largest software registry in the world, containing over a million packages. npm's package.json file defines project metadata and dependencies, enabling reproducible builds across different environments. Despite competition from alternatives like Yarn, npm remains the most widely used package manager in the JavaScript ecosystem.
npm (Node Package Manager) is the main tool for managing code packages in Node.js. It helps developers find, share, and use JavaScript code easily. Think of it as a big library where you can get tools and code bits for your projects. npm uses a file called `package.json` to keep track of what your project needs, making it easy to build your project anywhere. Even with other tools like Yarn, npm is still the most popular choice for JavaScript developers.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Nuxt.js
Nuxt.js is a higher-level framework built on top of Vue.js, designed to create universal or single-page Vue applications. It simplifies the development process by providing a structured directory layout, automatic routing, and server-side rendering capabilities out of the box. Nuxt.js offers features like static site generation, code splitting, and asynchronous data fetching. It supports both client-side and server-side rendering, allowing developers to choose the most appropriate approach for each page. Nuxt.js emphasizes developer experience and performance optimization, making it popular for building scalable, SEO-friendly Vue applications. Its modular architecture and extensive plugin ecosystem enable easy integration of additional functionalities.
Nuxt.js is a framework based on Vue.js for building web apps. It makes development easier with a ready-made structure, auto page routing, and server-side rendering. Nuxt.js can also create static sites, split code, and fetch data. You can choose how each page is rendered. It focuses on good developer experience and performance, making it popular for large, SEO-friendly Vue apps. It's also easy to add more features with its plugins.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Nuxt.js
Nuxt.js is a higher-level framework built on top of Vue.js, designed to create universal or single-page Vue applications. It simplifies the development process by providing a structured directory layout, automatic routing, and server-side rendering capabilities out of the box. Nuxt.js offers features like static site generation, code splitting, and asynchronous data fetching. It supports both client-side and server-side rendering, allowing developers to choose the most appropriate approach for each page. Nuxt.js emphasizes developer experience and performance optimization, making it popular for building scalable, SEO-friendly Vue applications. Its modular architecture and extensive plugin ecosystem enable easy integration of additional functionalities.
Nuxt.js is a framework based on Vue.js for building web apps. It makes development easier with a ready-made structure, auto page routing, and server-side rendering. Nuxt.js can also create static sites, split code, and fetch data. You can choose how each page is rendered. It focuses on good developer experience and performance, making it popular for large, SEO-friendly Vue apps. It's also easy to add more features with its plugins.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# OWASP Security Risks
OWASP (Open Web Application Security Project) identifies and ranks the most critical security risks to web applications. The OWASP Top 10 list includes vulnerabilities such as injection flaws, broken authentication, sensitive data exposure, XML external entities (XXE), broken access control, security misconfigurations, cross-site scripting (XSS), insecure deserialization, using components with known vulnerabilities, and insufficient logging and monitoring. These risks represent common attack vectors exploited by malicious actors to compromise web applications and their underlying systems. OWASP provides guidelines and best practices for mitigating these risks, emphasizing the importance of secure coding practices, regular security assessments, and implementing robust security controls throughout the software development lifecycle. Understanding and addressing these risks is crucial for developers and organizations to enhance the security posture of their web applications.
OWASP (Open Web Application Security Project) lists the biggest security dangers for web apps. The OWASP Top 10 includes things like injection attacks, bad authentication, data exposure, and using old, unsafe code. These are common ways hackers break into websites. OWASP gives advice on how to fix these problems by coding securely, checking for issues often, and using strong security from start to finish. Knowing these risks helps protect web apps.
Visit the following resources to learn more:

View File

@@ -1,6 +1,6 @@
# Package Managers
Package managers are tools that automate the process of installing, updating, configuring, and removing software packages in a consistent manner. They handle dependency resolution, version management, and package distribution for programming languages and operating systems. Popular package managers include npm for JavaScript, pip for Python, and apt for Debian-based Linux distributions. These tools maintain a centralized repository of packages, allowing developers to easily share and reuse code. Package managers simplify project setup, ensure consistency across development environments, and help manage complex dependency trees. They play a crucial role in modern software development by streamlining workflow, enhancing collaboration, and improving code reusability.
Package managers are tools that help install, update, and remove software pieces (packages). They manage versions and what other packages are needed. Examples are npm for JavaScript and pip for Python. They make it easy to share and reuse code by keeping packages in one place. Package managers simplify project setup and help keep things consistent. They are very important for modern software development by making work smoother and improving teamwork.
Visit the following resources to learn more:

Some files were not shown because too many files have changed in this diff Show More