mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-03-13 18:21:57 +08:00
Compare commits
2 Commits
roadmap/de
...
feat/share
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8407dd7448 | ||
|
|
d659752ad2 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -29,5 +29,6 @@ pnpm-debug.log*
|
||||
tests-examples
|
||||
*.csv
|
||||
|
||||
/editor/*
|
||||
!/editor/readonly-editor.tsx
|
||||
/renderer/*
|
||||
!/renderer/index.tsx
|
||||
!/renderer/renderer.ts
|
||||
3
.npmrc
3
.npmrc
@@ -1,2 +1 @@
|
||||
auto-install-peers=true
|
||||
strict-peer-dependencies=false
|
||||
auto-install-peers=true
|
||||
@@ -13,6 +13,6 @@ module.exports = {
|
||||
],
|
||||
plugins: [
|
||||
require.resolve('prettier-plugin-astro'),
|
||||
'prettier-plugin-tailwindcss',
|
||||
require('prettier-plugin-tailwindcss'),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
export function ReadonlyEditor(props: any) {
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 top-0 z-[9999] border bg-white p-5 text-black">
|
||||
<h2 className="mb-2 text-xl font-semibold">Private Component</h2>
|
||||
<p className="mb-4">
|
||||
Renderer is a private component. If you are a collaborator and have
|
||||
access to it. Run the following command:
|
||||
</p>
|
||||
<code className="mt-5 rounded-md bg-gray-800 p-2 text-white">
|
||||
npm run generate-renderer
|
||||
</code>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -11025,4 +11025,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
61
package.json
61
package.json
@@ -16,54 +16,53 @@
|
||||
"roadmap-links": "node scripts/roadmap-links.cjs",
|
||||
"roadmap-dirs": "node scripts/roadmap-dirs.cjs",
|
||||
"roadmap-content": "node scripts/roadmap-content.cjs",
|
||||
"generate-renderer": "sh scripts/generate-renderer.sh",
|
||||
"best-practice-dirs": "node scripts/best-practice-dirs.cjs",
|
||||
"best-practice-content": "node scripts/best-practice-content.cjs",
|
||||
"generate-renderer": "sh scripts/generate-renderer.sh",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/react": "^3.0.3",
|
||||
"@astrojs/sitemap": "^3.0.2",
|
||||
"@astrojs/tailwind": "^5.0.2",
|
||||
"@fingerprintjs/fingerprintjs": "^4.1.0",
|
||||
"@astrojs/react": "^3.0.0",
|
||||
"@astrojs/sitemap": "^1.3.3",
|
||||
"@astrojs/tailwind": "^5.0.0",
|
||||
"@fingerprintjs/fingerprintjs": "^3.4.1",
|
||||
"@nanostores/react": "^0.7.1",
|
||||
"@types/react": "^18.2.31",
|
||||
"@types/react-dom": "^18.2.14",
|
||||
"astro": "^3.3.3",
|
||||
"astro-compress": "^2.1.5",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"astro": "^3.0.5",
|
||||
"astro-compress": "^2.0.8",
|
||||
"clsx": "^2.0.0",
|
||||
"dracula-prism": "^2.1.13",
|
||||
"jose": "^4.15.4",
|
||||
"jose": "^4.14.4",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.288.0",
|
||||
"nanoid": "^5.0.2",
|
||||
"nanostores": "^0.9.4",
|
||||
"node-html-parser": "^6.1.10",
|
||||
"npm-check-updates": "^16.14.6",
|
||||
"lucide-react": "^0.274.0",
|
||||
"nanoid": "^4.0.2",
|
||||
"nanostores": "^0.9.2",
|
||||
"node-html-parser": "^6.1.5",
|
||||
"npm-check-updates": "^16.10.12",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.2.0",
|
||||
"react": "^18.0.0",
|
||||
"react-confetti": "^6.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"reactflow": "^11.9.4",
|
||||
"rehype-external-links": "^3.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"reactflow": "^11.8.3",
|
||||
"rehype-external-links": "^2.1.0",
|
||||
"roadmap-renderer": "^1.0.6",
|
||||
"slugify": "^1.6.6",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"zustand": "^4.4.4"
|
||||
"tailwindcss": "^3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.39.0",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/js-cookie": "^3.0.5",
|
||||
"@types/prismjs": "^1.26.2",
|
||||
"@playwright/test": "^1.35.1",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"csv-parser": "^3.0.0",
|
||||
"gh-pages": "^6.0.0",
|
||||
"gh-pages": "^5.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"markdown-it": "^13.0.2",
|
||||
"openai": "^4.13.0",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-plugin-astro": "^0.12.0",
|
||||
"prettier-plugin-tailwindcss": "^0.5.6"
|
||||
"markdown-it": "^13.0.1",
|
||||
"openai": "^3.3.0",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-astro": "^0.10.0",
|
||||
"prettier-plugin-tailwindcss": "^0.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
2297
pnpm-lock.yaml
generated
2297
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 448 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 561 KiB After Width: | Height: | Size: 93 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 614 KiB |
10
readme.md
10
readme.md
@@ -24,7 +24,7 @@
|
||||
|
||||
Roadmaps are now interactive, you can click the nodes to read more about the topics.
|
||||
|
||||
### [View all Roadmaps](https://roadmap.sh) · [Best Practices](https://roadmap.sh/best-practices) · [Questions](https://roadmap.sh/questions)
|
||||
### [View all Roadmaps](https://roadmap.sh)
|
||||
|
||||

|
||||
|
||||
@@ -39,7 +39,6 @@ Here is the list of available roadmaps with more being actively worked upon.
|
||||
- [QA Roadmap](https://roadmap.sh/qa)
|
||||
- [Python Roadmap](https://roadmap.sh/python)
|
||||
- [Software Architect Roadmap](https://roadmap.sh/software-architect)
|
||||
- [Game Developer Roadmap](https://roadmap.sh/game-developer)
|
||||
- [Software Design and Architecture Roadmap](https://roadmap.sh/software-design-architecture)
|
||||
- [JavaScript Roadmap](https://roadmap.sh/javascript)
|
||||
- [TypeScript Roadmap](https://roadmap.sh/typescript)
|
||||
@@ -68,18 +67,13 @@ Here is the list of available roadmaps with more being actively worked upon.
|
||||
- [Docker Roadmap](https://roadmap.sh/docker)
|
||||
- [Prompt Engineering Roadmap](https://roadmap.sh/prompt-engineering)
|
||||
|
||||
There are also interactive best practices:
|
||||
We have also added a new form of visual content covering best practices:
|
||||
|
||||
- [Code Review Best Practices](https://roadmap.sh/best-practices/code-review)
|
||||
- [Frontend Performance Best Practices](https://roadmap.sh/best-practices/frontend-performance)
|
||||
- [API Security Best Practices](https://roadmap.sh/best-practices/api-security)
|
||||
- [AWS Best Practices](https://roadmap.sh/best-practices/aws)
|
||||
|
||||
..and questions to help you test, rate and improve your knowledge
|
||||
|
||||
- [JavaScript Questions](https://roadmap.sh/questions/javascript)
|
||||
- [React Questions](https://roadmap.sh/questions/react)
|
||||
|
||||

|
||||
|
||||
## Share with the community
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-#!/usr/bin/env bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
@@ -8,17 +8,17 @@ if [ ! -d ".temp/web-draw" ]; then
|
||||
git clone git@github.com:roadmapsh/web-draw.git .temp/web-draw
|
||||
fi
|
||||
|
||||
rm -rf editor
|
||||
mkdir editor
|
||||
rm -rf renderer
|
||||
mkdir renderer
|
||||
|
||||
# copy the files at /src/editor/* to /editor
|
||||
# copy the files at /src/editor/renderer/* to /renderer
|
||||
# while replacing any existing files
|
||||
cp -rf .temp/web-draw/src/editor/* editor
|
||||
cp -rf .temp/web-draw/src/editor/renderer/* renderer
|
||||
|
||||
# Add @ts-nocheck to the top of each ts and tsx file
|
||||
# so that the typescript compiler doesn't complain
|
||||
# about the missing types
|
||||
find editor -type f \( -name "*.ts" -o -name "*.tsx" \) -print0 | while IFS= read -r -d '' file; do
|
||||
find renderer -type f \( -name "*.ts" -o -name "*.tsx" \) -print0 | while IFS= read -r -d '' file; do
|
||||
if [ -f "$file" ]; then
|
||||
echo "// @ts-nocheck" > temp
|
||||
cat "$file" >> temp
|
||||
@@ -28,5 +28,6 @@ find editor -type f \( -name "*.ts" -o -name "*.tsx" \) -print0 | while IFS= rea
|
||||
done
|
||||
|
||||
|
||||
# ignore the worktree changes for the editor directory
|
||||
git update-index --assume-unchanged editor/readonly-editor.tsx
|
||||
|
||||
# ignore the worktree changes for the renderer directory
|
||||
git update-index --skip-worktree renderer/*
|
||||
|
||||
@@ -19,12 +19,13 @@ if (!allowedRoadmapIds.includes(roadmapId)) {
|
||||
}
|
||||
|
||||
const ROADMAP_CONTENT_DIR = path.join(ALL_ROADMAPS_DIR, roadmapId, 'content');
|
||||
const OpenAI = require('openai');
|
||||
|
||||
const openai = new OpenAI({
|
||||
const { Configuration, OpenAIApi } = require('openai');
|
||||
const configuration = new Configuration({
|
||||
apiKey: OPEN_AI_API_KEY,
|
||||
});
|
||||
|
||||
const openai = new OpenAIApi(configuration);
|
||||
|
||||
function getFilesInFolder(folderPath, fileList = {}) {
|
||||
const files = fs.readdirSync(folderPath);
|
||||
|
||||
@@ -59,16 +60,16 @@ function writeTopicContent(currTopicUrl) {
|
||||
|
||||
const roadmapTitle = roadmapId.replace(/-/g, ' ');
|
||||
|
||||
let prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${childTopic}". Write me a brief paragraph for that. Content should be in markdown. I already know the benefits of each so do not add benefits in the output.`;
|
||||
let prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${childTopic}". Write me with a brief summary of that. Content should be in markdown. I already know the benefits of each so do not add benefits in the output. Also include the code examples if applicable to this topic.`;
|
||||
if (!childTopic) {
|
||||
prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${parentTopic}". Write me a brief paragraph for that. Content should be in markdown. I already know the benefits of each so do not add benefits in the output.`;
|
||||
prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${parentTopic}". Write me with a brief summary of that. Content should be in markdown. I already know the benefits of each so do not add benefits in the output. Also include the code examples if applicable to this topic.`;
|
||||
}
|
||||
|
||||
console.log(`Generating '${childTopic || parentTopic}'...`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
openai.chat.completions
|
||||
.create({
|
||||
openai
|
||||
.createChatCompletion({
|
||||
model: 'gpt-4',
|
||||
messages: [
|
||||
{
|
||||
@@ -78,7 +79,7 @@ function writeTopicContent(currTopicUrl) {
|
||||
],
|
||||
})
|
||||
.then((response) => {
|
||||
const article = response.choices[0].message.content;
|
||||
const article = response.data.choices[0].message.content;
|
||||
|
||||
resolve(article);
|
||||
})
|
||||
@@ -91,7 +92,7 @@ function writeTopicContent(currTopicUrl) {
|
||||
async function writeFileForGroup(group, topicUrlToPathMapping) {
|
||||
const topicId = group?.properties?.controlName;
|
||||
const topicTitle = group?.children?.controls?.control?.find(
|
||||
(control) => control?.typeID === 'Label',
|
||||
(control) => control?.typeID === 'Label'
|
||||
)?.properties?.text;
|
||||
const currTopicUrl = topicId?.replace(/^\d+-/g, '/')?.replace(/:/g, '/');
|
||||
if (!currTopicUrl) {
|
||||
@@ -137,14 +138,15 @@ async function writeFileForGroup(group, topicUrlToPathMapping) {
|
||||
async function run() {
|
||||
const topicUrlToPathMapping = getFilesInFolder(ROADMAP_CONTENT_DIR);
|
||||
|
||||
const roadmapJson = require(
|
||||
path.join(ALL_ROADMAPS_DIR, `${roadmapId}/${roadmapId}`),
|
||||
);
|
||||
const roadmapJson = require(path.join(
|
||||
ALL_ROADMAPS_DIR,
|
||||
`${roadmapId}/${roadmapId}`
|
||||
));
|
||||
|
||||
const groups = roadmapJson?.mockup?.controls?.control?.filter(
|
||||
(control) =>
|
||||
control.typeID === '__group__' &&
|
||||
!control.properties?.controlName?.startsWith('ext_link'),
|
||||
!control.properties?.controlName?.startsWith('ext_link')
|
||||
);
|
||||
|
||||
if (!OPEN_AI_API_KEY) {
|
||||
|
||||
@@ -97,7 +97,7 @@ const sidebarLinks = [
|
||||
}`}
|
||||
>
|
||||
<AstroIcon icon={'users'} class={`h-4 w-4 mr-2`} />
|
||||
Teams
|
||||
Teams
|
||||
</a>
|
||||
</li>
|
||||
{
|
||||
@@ -167,12 +167,13 @@ const sidebarLinks = [
|
||||
{sidebarLink.title}
|
||||
</span>
|
||||
|
||||
{sidebarLink.isNew && !isActive && (
|
||||
<span class='relative mr-1 flex items-center'>
|
||||
<span class='relative rounded-full bg-gray-200 p-1 text-xs' />
|
||||
<span class='absolute bottom-0 left-0 right-0 top-0 animate-ping rounded-full bg-gray-400 p-1 text-xs' />
|
||||
</span>
|
||||
)}
|
||||
{sidebarLink.isNew &&
|
||||
!isActive && (
|
||||
<span class='relative mr-1 flex items-center'>
|
||||
<span class='relative rounded-full bg-gray-200 p-1 text-xs' />
|
||||
<span class='absolute bottom-0 left-0 right-0 top-0 animate-ping rounded-full bg-gray-400 p-1 text-xs' />
|
||||
</span>
|
||||
)}
|
||||
|
||||
{sidebarLink.id === 'friends' && (
|
||||
<SidebarFriendsCounter client:load />
|
||||
|
||||
@@ -21,7 +21,7 @@ export function EmailLoginForm() {
|
||||
{
|
||||
email,
|
||||
password,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Log the user in and reload the page
|
||||
@@ -39,7 +39,7 @@ export function EmailLoginForm() {
|
||||
// @todo use proper types
|
||||
if ((error as any).type === 'user_not_verified') {
|
||||
window.location.href = `/verification-pending?email=${encodeURIComponent(
|
||||
email,
|
||||
email
|
||||
)}`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { useKeydown } from '../../hooks/use-keydown';
|
||||
import type { TeamResourceConfig } from './RoadmapSelector';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import {replaceChildren} from "../../lib/dom.ts";
|
||||
|
||||
export type ProgressMapProps = {
|
||||
teamId: string;
|
||||
@@ -82,8 +81,7 @@ export function UpdateTeamResourceModal(props: ProgressMapProps) {
|
||||
fontURL: '/fonts/balsamiq.woff2',
|
||||
});
|
||||
|
||||
replaceChildren(containerEl.current!, svg);
|
||||
// containerEl.current?.replaceChildren(svg);
|
||||
containerEl.current?.replaceChildren(svg);
|
||||
|
||||
// Render team configuration
|
||||
removedItems.forEach((topicId: string) => {
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { httpGet, httpPost } from '../../lib/http';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import { GitFork, Loader2, Map } from 'lucide-react';
|
||||
import { showLoginPopup } from '../../lib/popup';
|
||||
import type { RoadmapDocument } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx';
|
||||
|
||||
type CreateVersionProps = {
|
||||
roadmapId: string;
|
||||
};
|
||||
|
||||
export function CreateVersion(props: CreateVersionProps) {
|
||||
const { roadmapId } = props;
|
||||
|
||||
const toast = useToast();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isConfirming, setIsConfirming] = useState(false);
|
||||
const [userVersion, setUserVersion] = useState<RoadmapDocument>();
|
||||
|
||||
async function loadMyVersion() {
|
||||
if (!isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const { response, error } = await httpGet<RoadmapDocument>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-my-version/${roadmapId}`,
|
||||
{},
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
setUserVersion(response);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadMyVersion().finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
async function createVersion() {
|
||||
if (isCreating || !roadmapId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isLoggedIn()) {
|
||||
showLoginPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCreating(true);
|
||||
const { response, error } = await httpPost<{ roadmapId: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-create-version/${roadmapId}`,
|
||||
{},
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
setIsCreating(false);
|
||||
toast.error(error?.message || 'Failed to create version');
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = `${
|
||||
import.meta.env.PUBLIC_EDITOR_APP_URL
|
||||
}/${response?.roadmapId}`;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-[30px] w-[312px] animate-pulse rounded-md bg-gray-300"></div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoading && userVersion?._id) {
|
||||
return (
|
||||
<div className={'flex items-center'}>
|
||||
<a
|
||||
href={`/r?id=${userVersion._id}`}
|
||||
className="flex items-center rounded-md border border-blue-400 bg-gray-50 px-2.5 py-1 text-xs font-medium text-blue-600 hover:bg-blue-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:hover:bg-gray-100 max-sm:hidden sm:text-sm"
|
||||
>
|
||||
<Map size="15px" className="mr-1.5" />
|
||||
Visit your own version of this Roadmap
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isConfirming) {
|
||||
return (
|
||||
<p className="flex h-[30px] items-center text-sm text-red-500">
|
||||
Create and edit a custom roadmap from this roadmap?
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsConfirming(false);
|
||||
createVersion().finally(() => null);
|
||||
}}
|
||||
className="ml-2 font-semibold underline underline-offset-2"
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
<span className="text-xs"> / </span>
|
||||
<button
|
||||
className="font-semibold underline underline-offset-2"
|
||||
onClick={() => setIsConfirming(false)}
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
disabled={isCreating}
|
||||
className="flex items-center justify-center rounded-md border border-gray-300 bg-gray-50 px-2.5 py-1 text-xs font-medium text-black hover:bg-gray-200 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:hover:bg-gray-100 max-sm:hidden sm:text-sm"
|
||||
onClick={() => {
|
||||
if (!isLoggedIn()) {
|
||||
showLoginPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
setIsConfirming(true);
|
||||
}}
|
||||
>
|
||||
{isCreating ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-3 w-3 animate-spin stroke-[2.5]" />
|
||||
Please wait ..
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<GitFork className="mr-1.5" size="16px" />
|
||||
Create your own version of this roadmap
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export function CreateRoadmapButton(props: CreateRoadmapButtonProps) {
|
||||
<button
|
||||
className={cn(
|
||||
'flex h-full w-full items-center justify-center gap-1 overflow-hidden rounded-md border border-dashed border-gray-800 p-3 text-sm text-gray-400 hover:border-gray-600 hover:bg-gray-900 hover:text-gray-300',
|
||||
className,
|
||||
className
|
||||
)}
|
||||
onClick={toggleCreateRoadmapHandler}
|
||||
>
|
||||
|
||||
@@ -29,7 +29,6 @@ export interface RoadmapDocument {
|
||||
description?: string;
|
||||
creatorId: string;
|
||||
teamId?: string;
|
||||
isDiscoverable: boolean;
|
||||
type: AllowedCustomRoadmapType;
|
||||
visibility: AllowedRoadmapVisibility;
|
||||
sharedFriendIds?: string[];
|
||||
@@ -62,7 +61,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
|
||||
|
||||
async function handleSubmit(
|
||||
e: FormEvent<HTMLFormElement> | MouseEvent<HTMLButtonElement>,
|
||||
redirect: boolean = true,
|
||||
redirect: boolean = true
|
||||
) {
|
||||
e.preventDefault();
|
||||
if (isLoading) {
|
||||
@@ -85,7 +84,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
|
||||
}),
|
||||
nodes: [],
|
||||
edges: [],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
@@ -96,9 +95,9 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
|
||||
|
||||
toast.success('Roadmap created successfully');
|
||||
if (redirect) {
|
||||
window.location.href = `${
|
||||
import.meta.env.PUBLIC_EDITOR_APP_URL
|
||||
}/${response?._id}`;
|
||||
window.location.href = `${import.meta.env.PUBLIC_EDITOR_APP_URL}/${
|
||||
response?._id
|
||||
}`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -145,7 +144,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
|
||||
name="title"
|
||||
id="title"
|
||||
required
|
||||
className="block text-black w-full rounded-md border border-gray-300 px-2.5 py-2 outline-none focus:border-black sm:text-sm"
|
||||
className="block w-full rounded-md border border-gray-300 px-2.5 py-2 outline-none focus:border-black sm:text-sm"
|
||||
placeholder="Enter Title"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
@@ -165,7 +164,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
|
||||
name="description"
|
||||
required
|
||||
className={cn(
|
||||
'block text-black h-24 w-full resize-none rounded-md border border-gray-300 px-2.5 py-2 outline-none focus:border-black sm:text-sm',
|
||||
'block h-24 w-full resize-none rounded-md border border-gray-300 px-2.5 py-2 outline-none focus:border-black sm:text-sm',
|
||||
isInvalidDescription && 'border-red-300 bg-red-100'
|
||||
)}
|
||||
placeholder="Enter Description"
|
||||
@@ -186,7 +185,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
|
||||
type="button"
|
||||
className={cn(
|
||||
'block h-9 rounded-md border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-black outline-none hover:border-gray-300 hover:bg-gray-50 focus:border-gray-300 focus:bg-gray-100',
|
||||
!teamId && 'w-full',
|
||||
!teamId && 'w-full'
|
||||
)}
|
||||
>
|
||||
Cancel
|
||||
@@ -213,7 +212,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
|
||||
type="submit"
|
||||
className={cn(
|
||||
'flex h-9 items-center justify-center rounded-md border border-transparent bg-black px-4 py-2 text-sm font-medium text-white outline-none hover:bg-gray-800 focus:bg-gray-800',
|
||||
teamId ? 'hidden sm:flex' : 'w-full',
|
||||
teamId ? 'hidden sm:flex' : 'w-full'
|
||||
)}
|
||||
>
|
||||
{isLoading ? (
|
||||
|
||||
@@ -7,12 +7,13 @@ import {
|
||||
httpPost,
|
||||
} from '../../lib/http';
|
||||
import { RoadmapHeader } from './RoadmapHeader';
|
||||
import { RoadmapRenderer } from './RoadmapRenderer';
|
||||
import { TopicDetail } from '../TopicDetail/TopicDetail';
|
||||
import type { RoadmapDocument } from './CreateRoadmap/CreateRoadmapModal';
|
||||
import { currentRoadmap } from '../../stores/roadmap';
|
||||
import { UserProgressModal } from '../UserProgress/UserProgressModal';
|
||||
import { RestrictedPage } from './RestrictedPage';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import { FlowRoadmapRenderer } from './FlowRoadmapRenderer';
|
||||
|
||||
export const allowedLinkTypes = [
|
||||
'video',
|
||||
@@ -120,8 +121,13 @@ export function CustomRoadmap() {
|
||||
return (
|
||||
<>
|
||||
<RoadmapHeader />
|
||||
<FlowRoadmapRenderer roadmap={roadmap!} />
|
||||
<RoadmapRenderer roadmap={roadmap!} />
|
||||
<TopicDetail canSubmitContribution={false} />
|
||||
<UserProgressModal
|
||||
resourceId={roadmap?._id!}
|
||||
resourceType="roadmap"
|
||||
isCustomResource={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { CircleSlash, PenSquare, Shapes } from 'lucide-react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import {CircleSlash, PenSquare, Shapes} from 'lucide-react';
|
||||
|
||||
type EmptyRoadmapProps = {
|
||||
roadmapId: string;
|
||||
canManage: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function EmptyRoadmap(props: EmptyRoadmapProps) {
|
||||
const { roadmapId, canManage, className } = props;
|
||||
const { roadmapId, canManage } = props;
|
||||
const editUrl = `${import.meta.env.PUBLIC_EDITOR_APP_URL}/${roadmapId}`;
|
||||
|
||||
return (
|
||||
<div className={cn('flex h-full items-center justify-center', className)}>
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<CircleSlash className="mx-auto h-20 w-20 text-gray-400" />
|
||||
<h3 className="mt-2">This roadmap is currently empty.</h3>
|
||||
@@ -20,9 +18,9 @@ export function EmptyRoadmap(props: EmptyRoadmapProps) {
|
||||
{canManage && (
|
||||
<a
|
||||
href={editUrl}
|
||||
className="mt-4 flex items-center rounded-md bg-gray-500 px-4 py-2 font-medium text-white hover:bg-gray-600"
|
||||
className="mt-4 rounded-md bg-gray-500 px-4 py-2 font-medium text-white hover:bg-gray-600 flex items-center"
|
||||
>
|
||||
<Shapes className="mr-2 inline-block h-4 w-4" />
|
||||
<Shapes className="inline-block mr-2 h-4 w-4" />
|
||||
Edit Roadmap
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
import { ReadonlyEditor } from '../../../editor/readonly-editor';
|
||||
import type { RoadmapDocument } from './CreateRoadmap/CreateRoadmapModal';
|
||||
import {
|
||||
renderResourceProgress,
|
||||
updateResourceProgress,
|
||||
type ResourceProgressType,
|
||||
renderTopicProgress,
|
||||
refreshProgressCounters,
|
||||
} from '../../lib/resource-progress';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import type { Node } from 'reactflow';
|
||||
import { useCallback, type MouseEvent, useMemo, useState, useRef } from 'react';
|
||||
import { EmptyRoadmap } from './EmptyRoadmap';
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
type FlowRoadmapRendererProps = {
|
||||
roadmap: RoadmapDocument;
|
||||
};
|
||||
|
||||
export function FlowRoadmapRenderer(props: FlowRoadmapRendererProps) {
|
||||
const { roadmap } = props;
|
||||
const roadmapId = String(roadmap._id!);
|
||||
|
||||
const [hideRenderer, setHideRenderer] = useState(false);
|
||||
const editorWrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
async function updateTopicStatus(
|
||||
topicId: string,
|
||||
newStatus: ResourceProgressType,
|
||||
) {
|
||||
pageProgressMessage.set('Updating progress');
|
||||
updateResourceProgress(
|
||||
{
|
||||
resourceId: roadmapId,
|
||||
resourceType: 'roadmap',
|
||||
topicId,
|
||||
},
|
||||
newStatus,
|
||||
)
|
||||
.then(() => {
|
||||
renderTopicProgress(topicId, newStatus);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('Something went wrong, please try again.');
|
||||
console.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
refreshProgressCounters();
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const handleTopicRightClick = useCallback((e: MouseEvent, node: Node) => {
|
||||
const target = e?.currentTarget as HTMLDivElement;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentStatusDone = target?.classList.contains('done');
|
||||
updateTopicStatus(node.id, isCurrentStatusDone ? 'pending' : 'done');
|
||||
}, []);
|
||||
|
||||
const handleTopicShiftClick = useCallback((e: MouseEvent, node: Node) => {
|
||||
const target = e?.currentTarget as HTMLDivElement;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentStatusLearning = target?.classList.contains('learning');
|
||||
updateTopicStatus(
|
||||
node.id,
|
||||
isCurrentStatusLearning ? 'pending' : 'learning',
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleTopicAltClick = useCallback((e: MouseEvent, node: Node) => {
|
||||
const target = e?.currentTarget as HTMLDivElement;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentStatusSkipped = target?.classList.contains('skipped');
|
||||
updateTopicStatus(node.id, isCurrentStatusSkipped ? 'pending' : 'skipped');
|
||||
}, []);
|
||||
|
||||
const handleTopicClick = useCallback((e: MouseEvent, node: Node) => {
|
||||
const target = e?.currentTarget as HTMLDivElement;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('roadmap.node.click', {
|
||||
detail: {
|
||||
topicId: node.id,
|
||||
resourceId: roadmapId,
|
||||
resourceType: 'roadmap',
|
||||
isCustomResource: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleLinkClick = useCallback((linkId: string, href: string) => {
|
||||
if (!href) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isExternalLink = href.startsWith('http');
|
||||
if (isExternalLink) {
|
||||
window.open(href, '_blank');
|
||||
} else {
|
||||
window.location.href = href;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{hideRenderer && (
|
||||
<EmptyRoadmap
|
||||
roadmapId={roadmapId}
|
||||
canManage={roadmap.canManage}
|
||||
className="grow"
|
||||
/>
|
||||
)}
|
||||
<ReadonlyEditor
|
||||
ref={editorWrapperRef}
|
||||
roadmap={roadmap}
|
||||
className={cn(
|
||||
roadmap?.nodes?.length === 0
|
||||
? 'grow'
|
||||
: 'min-h-0 max-md:min-h-[1000px]',
|
||||
)}
|
||||
onRendered={() => {
|
||||
renderResourceProgress('roadmap', roadmapId).then(() => {
|
||||
if (roadmap?.nodes?.length === 0) {
|
||||
setHideRenderer(true);
|
||||
editorWrapperRef?.current?.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}}
|
||||
onTopicClick={handleTopicClick}
|
||||
onTopicRightClick={handleTopicRightClick}
|
||||
onTopicShiftClick={handleTopicShiftClick}
|
||||
onTopicAltClick={handleTopicAltClick}
|
||||
onButtonNodeClick={handleLinkClick}
|
||||
onLinkClick={handleLinkClick}
|
||||
fontFamily="Balsamiq Sans"
|
||||
fontURL="/fonts/balsamiq.woff2"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Globe,
|
||||
LockIcon,
|
||||
Users,
|
||||
PenSquare,
|
||||
} from 'lucide-react';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import {
|
||||
@@ -61,7 +60,6 @@ export function PersonalRoadmapList(props: PersonalRoadmapListType) {
|
||||
|
||||
const shareSettingsModal = selectedRoadmap && (
|
||||
<ShareOptionsModal
|
||||
isDiscoverable={selectedRoadmap.isDiscoverable}
|
||||
description={selectedRoadmap.description}
|
||||
visibility={selectedRoadmap.visibility}
|
||||
sharedFriendIds={selectedRoadmap.sharedFriendIds}
|
||||
@@ -143,7 +141,7 @@ function CustomRoadmapItem(props: CustomRoadmapItemProps) {
|
||||
|
||||
return (
|
||||
<li
|
||||
className="grid grid-cols-1 p-2.5 sm:grid-cols-[auto_172px]"
|
||||
className="grid grid-cols-1 p-2.5 sm:grid-cols-[auto_110px]"
|
||||
key={roadmap._id!}
|
||||
>
|
||||
<div className="mb-3 grid grid-cols-1 sm:mb-0">
|
||||
@@ -175,20 +173,10 @@ function CustomRoadmapItem(props: CustomRoadmapItemProps) {
|
||||
}}
|
||||
/>
|
||||
|
||||
<a
|
||||
href={editorLink}
|
||||
className={
|
||||
'ml-2 flex items-center gap-2 rounded-md border border-gray-300 bg-white px-2.5 py-1.5 text-xs text-black hover:bg-gray-50 focus:outline-none'
|
||||
}
|
||||
target={'_blank'}
|
||||
>
|
||||
<PenSquare className="inline-block h-4 w-4" />
|
||||
Edit
|
||||
</a>
|
||||
<a
|
||||
href={`/r?id=${roadmap._id}`}
|
||||
className={
|
||||
'ml-2 flex items-center gap-2 rounded-md border border-blue-400 bg-white px-2 py-1.5 text-xs hover:bg-blue-50 focus:outline-none text-blue-600'
|
||||
'ml-2 flex items-center gap-2 rounded-md border border-gray-300 bg-white px-2 py-1.5 text-xs hover:bg-gray-50 focus:outline-none'
|
||||
}
|
||||
target={'_blank'}
|
||||
>
|
||||
|
||||
@@ -24,7 +24,6 @@ export function ResourceProgressStats(props: ResourceProgressStatsProps) {
|
||||
<>
|
||||
{isSharing && $canManageCurrentRoadmap && $currentRoadmap && (
|
||||
<ShareOptionsModal
|
||||
isDiscoverable={$currentRoadmap.isDiscoverable}
|
||||
description={$currentRoadmap?.description}
|
||||
visibility={$currentRoadmap?.visibility}
|
||||
teamId={$currentRoadmap?.teamId}
|
||||
@@ -43,7 +42,7 @@ export function ResourceProgressStats(props: ResourceProgressStatsProps) {
|
||||
<div
|
||||
data-progress-nums-container=""
|
||||
className={cn(
|
||||
'striped-loader relative z-50 hidden items-center justify-between bg-white px-2 py-1.5 sm:flex',
|
||||
'striped-loader relative hidden items-center justify-between bg-white px-2 py-1.5 sm:flex',
|
||||
{
|
||||
'rounded-bl-md rounded-br-md': isSecondaryBanner,
|
||||
'rounded-md': !isSecondaryBanner,
|
||||
|
||||
@@ -23,7 +23,7 @@ export function RoadmapActionButton(props: RoadmapActionButtonProps) {
|
||||
<button
|
||||
disabled={false}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white py-1.5 pl-2 pr-2 text-xs font-medium text-black hover:border-gray-300 hover:bg-gray-300 sm:pl-1.5 sm:pr-3 sm:text-sm"
|
||||
className="inline-flex items-center justify-center rounded-md bg-gray-500 py-1.5 pl-2 pr-2 text-xs font-medium text-white hover:bg-gray-600 sm:pl-1.5 sm:pr-3 sm:text-sm"
|
||||
>
|
||||
<MoreVertical className="mr-0 h-4 w-4 stroke-[2.5] sm:mr-1.5" />
|
||||
<span className="hidden sm:inline">Actions</span>
|
||||
@@ -32,7 +32,7 @@ export function RoadmapActionButton(props: RoadmapActionButtonProps) {
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="align-right absolute right-0 top-full mt-1 w-[140px] rounded-md bg-slate-800 px-2 py-2 text-white shadow-md z-[9999]"
|
||||
className="align-right absolute right-0 top-full z-50 mt-1 w-[140px] rounded-md bg-slate-800 px-2 py-2 text-white shadow-md"
|
||||
>
|
||||
<ul>
|
||||
{onUpdateSharing && (
|
||||
|
||||
@@ -9,8 +9,6 @@ import { type TeamResourceConfig } from '../CreateTeam/RoadmapSelector';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { RoadmapActionButton } from './RoadmapActionButton';
|
||||
import { Lock, Shapes } from 'lucide-react';
|
||||
import { Modal } from '../Modal';
|
||||
import { ShareSuccess } from '../ShareOptions/ShareSuccess';
|
||||
|
||||
type RoadmapHeaderProps = {};
|
||||
|
||||
@@ -24,11 +22,9 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
|
||||
_id: roadmapId,
|
||||
creator,
|
||||
team,
|
||||
visibility,
|
||||
} = useStore(currentRoadmap) || {};
|
||||
|
||||
const [isSharing, setIsSharing] = useState(false);
|
||||
const [isSharingWithOthers, setIsSharingWithOthers] = useState(false);
|
||||
const toast = useToast();
|
||||
|
||||
async function deleteResource() {
|
||||
@@ -69,22 +65,6 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
|
||||
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${creator?.avatar}`
|
||||
: '/images/default-avatar.png';
|
||||
|
||||
const sharingWithOthersModal = isSharingWithOthers && (
|
||||
<Modal
|
||||
onClose={() => setIsSharingWithOthers(false)}
|
||||
wrapperClassName="max-w-lg"
|
||||
bodyClassName="p-4 flex flex-col"
|
||||
>
|
||||
<ShareSuccess
|
||||
visibility="public"
|
||||
roadmapId={roadmapId!}
|
||||
description={description}
|
||||
onClose={() => setIsSharingWithOthers(false)}
|
||||
isSharingWithOthers={true}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="border-b">
|
||||
<div className="container relative py-5 sm:py-12">
|
||||
@@ -102,7 +82,7 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
|
||||
</span>
|
||||
{team && (
|
||||
<>
|
||||
from
|
||||
in
|
||||
<span className="font-semibold text-gray-900">
|
||||
{team?.name}
|
||||
</span>
|
||||
@@ -137,78 +117,62 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
|
||||
<span className="ml-2">Subscribe</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{$canManageCurrentRoadmap && (
|
||||
<>
|
||||
{isSharing && $currentRoadmap && (
|
||||
<ShareOptionsModal
|
||||
isDiscoverable={$currentRoadmap.isDiscoverable}
|
||||
description={$currentRoadmap?.description}
|
||||
visibility={$currentRoadmap?.visibility}
|
||||
teamId={$currentRoadmap?.teamId}
|
||||
roadmapId={$currentRoadmap?._id!}
|
||||
sharedFriendIds={$currentRoadmap?.sharedFriendIds || []}
|
||||
sharedTeamMemberIds={
|
||||
$currentRoadmap?.sharedTeamMemberIds || []
|
||||
}
|
||||
onClose={() => setIsSharing(false)}
|
||||
onShareSettingsUpdate={(settings) => {
|
||||
currentRoadmap.set({
|
||||
...$currentRoadmap,
|
||||
...settings,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<a
|
||||
href={`${import.meta.env.PUBLIC_EDITOR_APP_URL}/${
|
||||
$currentRoadmap?._id
|
||||
}`}
|
||||
target="_blank"
|
||||
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white py-1.5 pl-2 pr-2 text-xs font-medium text-black hover:border-gray-300 hover:bg-gray-300 sm:px-3 sm:text-sm"
|
||||
>
|
||||
<Shapes className="mr-1.5 h-4 w-4 stroke-[2.5]" />
|
||||
<span className="hidden sm:inline-block">Edit Roadmap</span>
|
||||
<span className="sm:hidden">Edit</span>
|
||||
</a>
|
||||
<button
|
||||
onClick={() => setIsSharing(true)}
|
||||
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white py-1.5 pl-2 pr-2 text-xs font-medium text-black hover:border-gray-300 hover:bg-gray-300 sm:px-3 sm:text-sm"
|
||||
>
|
||||
<Lock className="mr-1.5 h-4 w-4 stroke-[2.5]" />
|
||||
Sharing
|
||||
</button>
|
||||
|
||||
<RoadmapActionButton
|
||||
onDelete={() => {
|
||||
const confirmation = window.confirm(
|
||||
'Are you sure you want to delete this roadmap?'
|
||||
);
|
||||
|
||||
if (!confirmation) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteResource().finally(() => null);
|
||||
{$canManageCurrentRoadmap && (
|
||||
<div className="flex items-center gap-2">
|
||||
{isSharing && $currentRoadmap && (
|
||||
<ShareOptionsModal
|
||||
description={$currentRoadmap?.description}
|
||||
visibility={$currentRoadmap?.visibility}
|
||||
teamId={$currentRoadmap?.teamId}
|
||||
roadmapId={$currentRoadmap?._id!}
|
||||
sharedFriendIds={$currentRoadmap?.sharedFriendIds || []}
|
||||
sharedTeamMemberIds={
|
||||
$currentRoadmap?.sharedTeamMemberIds || []
|
||||
}
|
||||
onClose={() => setIsSharing(false)}
|
||||
onShareSettingsUpdate={(settings) => {
|
||||
currentRoadmap.set({
|
||||
...$currentRoadmap,
|
||||
...settings,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
|
||||
{!$canManageCurrentRoadmap && visibility === 'public' && (
|
||||
<>
|
||||
{sharingWithOthersModal}
|
||||
<button
|
||||
onClick={() => setIsSharingWithOthers(true)}
|
||||
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white py-1.5 pl-2 pr-2 text-xs font-medium text-black hover:border-gray-300 hover:bg-gray-300 sm:px-3 sm:text-sm"
|
||||
>
|
||||
<Lock className="mr-1.5 h-4 w-4 stroke-[2.5]" />
|
||||
Share with Others
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<a
|
||||
href={`${import.meta.env.PUBLIC_EDITOR_APP_URL}/${
|
||||
$currentRoadmap?._id
|
||||
}`}
|
||||
target="_blank"
|
||||
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white py-1.5 pl-2 pr-2 text-xs font-medium text-black hover:border-gray-300 hover:bg-gray-300 sm:px-3 sm:text-sm"
|
||||
>
|
||||
<Shapes className="mr-1.5 h-4 w-4 stroke-[2.5]" />
|
||||
<span className="hidden sm:inline-block">Edit Roadmap</span>
|
||||
<span className="sm:hidden">Edit</span>
|
||||
</a>
|
||||
<button
|
||||
onClick={() => setIsSharing(true)}
|
||||
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white py-1.5 pl-2 pr-2 text-xs font-medium text-black hover:border-gray-300 hover:bg-gray-300 sm:px-3 sm:text-sm"
|
||||
>
|
||||
<Lock className="mr-1.5 h-4 w-4 stroke-[2.5]" />
|
||||
Sharing
|
||||
</button>
|
||||
|
||||
<RoadmapActionButton
|
||||
onDelete={() => {
|
||||
const confirmation = window.confirm(
|
||||
'Are you sure you want to delete this roadmap?'
|
||||
);
|
||||
|
||||
if (!confirmation) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteResource().finally(() => null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<RoadmapHint
|
||||
|
||||
53
src/components/CustomRoadmap/RoadmapRenderer.css
Normal file
53
src/components/CustomRoadmap/RoadmapRenderer.css
Normal file
@@ -0,0 +1,53 @@
|
||||
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 > g[data-type='link-item'],
|
||||
svg > g[data-type='button'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
svg > g[data-type='topic']:hover > rect {
|
||||
fill: #d6d700;
|
||||
}
|
||||
|
||||
svg > g[data-type='subtopic']:hover > rect {
|
||||
fill: #f3c950;
|
||||
}
|
||||
svg > g[data-type='button']:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
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 > 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;
|
||||
}
|
||||
177
src/components/CustomRoadmap/RoadmapRenderer.tsx
Normal file
177
src/components/CustomRoadmap/RoadmapRenderer.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Renderer } from '../../../renderer';
|
||||
import './RoadmapRenderer.css';
|
||||
import {
|
||||
renderResourceProgress,
|
||||
updateResourceProgress,
|
||||
type ResourceProgressType,
|
||||
renderTopicProgress,
|
||||
refreshProgressCounters,
|
||||
} from '../../lib/resource-progress';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import type { RoadmapDocument } from './CreateRoadmap/CreateRoadmapModal';
|
||||
import { EmptyRoadmap } from './EmptyRoadmap';
|
||||
|
||||
type RoadmapRendererProps = {
|
||||
roadmap: RoadmapDocument;
|
||||
};
|
||||
|
||||
type RoadmapNodeDetails = {
|
||||
nodeId: string;
|
||||
nodeType: string;
|
||||
targetGroup: SVGElement;
|
||||
};
|
||||
|
||||
export function getNodeDetails(
|
||||
svgElement: SVGElement
|
||||
): RoadmapNodeDetails | null {
|
||||
const targetGroup = (svgElement?.closest('g') as SVGElement) || {};
|
||||
|
||||
const nodeId = targetGroup?.dataset?.nodeId;
|
||||
const nodeType = targetGroup?.dataset?.type;
|
||||
if (!nodeId || !nodeType) return null;
|
||||
|
||||
return { nodeId, nodeType, targetGroup };
|
||||
}
|
||||
|
||||
export const allowedClickableNodeTypes = [
|
||||
'topic',
|
||||
'subtopic',
|
||||
'button',
|
||||
'link-item',
|
||||
];
|
||||
|
||||
export function RoadmapRenderer(props: RoadmapRendererProps) {
|
||||
const { roadmap } = props;
|
||||
const roadmapRef = useRef<HTMLDivElement>(null);
|
||||
const roadmapId = roadmap._id!;
|
||||
|
||||
const toast = useToast();
|
||||
const [hideRenderer, setHideRenderer] = useState(false);
|
||||
|
||||
async function updateTopicStatus(
|
||||
topicId: string,
|
||||
newStatus: ResourceProgressType
|
||||
) {
|
||||
pageProgressMessage.set('Updating progress');
|
||||
updateResourceProgress(
|
||||
{
|
||||
resourceId: roadmapId,
|
||||
resourceType: 'roadmap',
|
||||
topicId,
|
||||
},
|
||||
newStatus
|
||||
)
|
||||
.then(() => {
|
||||
renderTopicProgress(topicId, newStatus);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('Something went wrong, please try again.');
|
||||
console.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
refreshProgressCounters();
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const handleSvgClick = useCallback((e: MouseEvent) => {
|
||||
const target = e.target as SVGElement;
|
||||
const { nodeId, nodeType, targetGroup } = getNodeDetails(target) || {};
|
||||
if (!nodeId || !nodeType || !allowedClickableNodeTypes.includes(nodeType))
|
||||
return;
|
||||
|
||||
if (nodeType === 'button' || nodeType === 'link-item') {
|
||||
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 (e.shiftKey) {
|
||||
e.preventDefault();
|
||||
updateTopicStatus(
|
||||
nodeId,
|
||||
isCurrentStatusLearning ? 'pending' : 'learning'
|
||||
);
|
||||
return;
|
||||
} else if (e.altKey) {
|
||||
e.preventDefault();
|
||||
updateTopicStatus(nodeId, isCurrentStatusSkipped ? 'pending' : 'skipped');
|
||||
return;
|
||||
}
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('roadmap.node.click', {
|
||||
detail: {
|
||||
topicId: nodeId,
|
||||
resourceId: roadmap?._id,
|
||||
resourceType: 'roadmap',
|
||||
isCustomResource: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleSvgRightClick = useCallback((e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const target = e.target as SVGElement;
|
||||
const { nodeId, nodeType, targetGroup } = getNodeDetails(target) || {};
|
||||
if (!nodeId || !nodeType || !allowedClickableNodeTypes.includes(nodeType))
|
||||
return;
|
||||
|
||||
if (nodeType === 'button' || nodeType === 'link-item') {
|
||||
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 (
|
||||
<div className="flex grow bg-gray-50 pb-8 pt-4 sm:pt-12">
|
||||
<div className="container !max-w-[1000px]">
|
||||
<Renderer
|
||||
ref={roadmapRef}
|
||||
roadmap={{ nodes: roadmap?.nodes!, edges: roadmap?.edges! }}
|
||||
onRendered={() => {
|
||||
renderResourceProgress('roadmap', roadmapId).then(() => {
|
||||
if (roadmap?.nodes?.length === 0) {
|
||||
setHideRenderer(true);
|
||||
roadmapRef?.current?.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{hideRenderer && (
|
||||
<EmptyRoadmap roadmapId={roadmapId} canManage={roadmap.canManage} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -48,7 +48,7 @@ import Icon from './AstroIcon.astro';
|
||||
<span class='mx-2 text-gray-400'>by</span>
|
||||
<a
|
||||
class='font-regular rounded-md bg-blue-600 px-1.5 py-1 text-sm hover:bg-blue-700'
|
||||
href='https://twitter.com/kamrify'
|
||||
href='https://twitter.com/intent/user?screen_name=kamrify'
|
||||
target='_blank'
|
||||
>
|
||||
<span class='hidden sm:inline'>@kamrify</span>
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
import type { ResourceProgressType, ResourceType } from '../../lib/resource-progress';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import { showLoginPopup } from '../../lib/popup';
|
||||
import {replaceChildren} from "../../lib/dom.ts";
|
||||
|
||||
export class Renderer {
|
||||
resourceId: string;
|
||||
@@ -89,8 +88,7 @@ export class Renderer {
|
||||
});
|
||||
})
|
||||
.then((svg) => {
|
||||
replaceChildren(this.containerEl!, svg);
|
||||
// this.containerEl?.replaceChildren(svg);
|
||||
this.containerEl?.replaceChildren(svg);
|
||||
})
|
||||
.then(() => {
|
||||
return renderResourceProgress(
|
||||
|
||||
@@ -10,7 +10,6 @@ import { FriendProgressItem } from './FriendProgressItem';
|
||||
import UserIcon from '../../icons/user.svg';
|
||||
import { UserProgressModal } from '../UserProgress/UserProgressModal';
|
||||
import { InviteFriendPopup } from './InviteFriendPopup';
|
||||
import { UserCustomProgressModal } from '../UserProgress/UserCustomProgressModal';
|
||||
|
||||
type FriendResourceProgress = {
|
||||
updatedAt: string;
|
||||
@@ -108,25 +107,6 @@ export function FriendsPage() {
|
||||
return <EmptyFriends befriendUrl={befriendUrl} />;
|
||||
}
|
||||
|
||||
const progressModal =
|
||||
showFriendProgress && showFriendProgress?.isCustomResource ? (
|
||||
<UserCustomProgressModal
|
||||
userId={showFriendProgress?.friend.userId}
|
||||
resourceId={showFriendProgress.resourceId}
|
||||
resourceType="roadmap"
|
||||
isCustomResource={true}
|
||||
onClose={() => setShowFriendProgress(undefined)}
|
||||
/>
|
||||
) : (
|
||||
<UserProgressModal
|
||||
userId={showFriendProgress?.friend.userId}
|
||||
resourceId={showFriendProgress?.resourceId!}
|
||||
resourceType={'roadmap'}
|
||||
onClose={() => setShowFriendProgress(undefined)}
|
||||
isCustomResource={showFriendProgress?.isCustomResource}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showInviteFriendPopup && (
|
||||
@@ -136,7 +116,15 @@ export function FriendsPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{showFriendProgress && progressModal}
|
||||
{showFriendProgress && (
|
||||
<UserProgressModal
|
||||
userId={showFriendProgress.friend.userId}
|
||||
resourceId={showFriendProgress.resourceId}
|
||||
resourceType={'roadmap'}
|
||||
onClose={() => setShowFriendProgress(undefined)}
|
||||
isCustomResource={showFriendProgress.isCustomResource}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="mb-4 flex flex-col items-stretch justify-between gap-2 sm:flex-row sm:items-center sm:gap-0">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -4,14 +4,12 @@ import { isLoggedIn } from '../../lib/jwt';
|
||||
import { AccountDropdownList } from './AccountDropdownList';
|
||||
import { DropdownTeamList } from './DropdownTeamList';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx';
|
||||
|
||||
export function AccountDropdown() {
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const [isTeamsOpen, setIsTeamsOpen] = useState(false);
|
||||
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false);
|
||||
|
||||
useOutsideClick(dropdownRef, () => {
|
||||
setShowDropdown(false);
|
||||
@@ -24,14 +22,6 @@ export function AccountDropdown() {
|
||||
|
||||
return (
|
||||
<div className="relative z-50 animate-fade-in">
|
||||
{isCreatingRoadmap && (
|
||||
<CreateRoadmapModal
|
||||
onClose={() => {
|
||||
setIsCreatingRoadmap(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="flex h-8 w-40 items-center justify-center gap-1.5 rounded-full bg-gradient-to-r from-purple-500 to-purple-700 px-4 py-2 text-sm font-medium text-white hover:from-purple-500 hover:to-purple-600"
|
||||
onClick={() => {
|
||||
@@ -53,13 +43,7 @@ export function AccountDropdown() {
|
||||
{isTeamsOpen ? (
|
||||
<DropdownTeamList setIsTeamsOpen={setIsTeamsOpen} />
|
||||
) : (
|
||||
<AccountDropdownList
|
||||
onCreateRoadmap={() => {
|
||||
setIsCreatingRoadmap(true);
|
||||
setShowDropdown(false);
|
||||
}}
|
||||
setIsTeamsOpen={setIsTeamsOpen}
|
||||
/>
|
||||
<AccountDropdownList setIsTeamsOpen={setIsTeamsOpen} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,76 +1,54 @@
|
||||
import { ChevronRight, LogOut, Map, Plus, User2, Users2 } from 'lucide-react';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { logout } from './navigation';
|
||||
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx';
|
||||
import { useState } from 'react';
|
||||
|
||||
type AccountDropdownListProps = {
|
||||
onCreateRoadmap: () => void;
|
||||
setIsTeamsOpen: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
export function AccountDropdownList(props: AccountDropdownListProps) {
|
||||
const { setIsTeamsOpen, onCreateRoadmap } = props;
|
||||
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false);
|
||||
const { setIsTeamsOpen } = props;
|
||||
|
||||
return (
|
||||
<ul>
|
||||
<li className="px-1">
|
||||
<a
|
||||
href="/account"
|
||||
className="group flex items-center gap-2 rounded py-2 pl-3 pr-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
|
||||
className="block rounded pl-4 pr-2 py-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
|
||||
>
|
||||
<User2 className="h-4 w-4 stroke-[2.5px] text-slate-400 group-hover:text-white" />
|
||||
Profile
|
||||
</a>
|
||||
</li>
|
||||
<li className="px-1">
|
||||
<a
|
||||
href="/account/friends"
|
||||
className="group flex items-center gap-2 rounded py-2 pl-3 pr-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
|
||||
className="block rounded pl-4 pr-2 py-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
|
||||
>
|
||||
<Users2 className="h-4 w-4 stroke-[2px] text-slate-400 group-hover:text-white" />
|
||||
Friends
|
||||
</a>
|
||||
</li>
|
||||
<li className="mt-1 border-t border-t-gray-700/60 px-1 pt-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
onCreateRoadmap();
|
||||
}}
|
||||
className="group flex w-full items-center gap-2 rounded py-2 pl-3 pr-2 text-left text-sm font-medium text-slate-100 hover:bg-slate-700"
|
||||
>
|
||||
<Plus className="h-4 w-4 stroke-[2px] text-slate-400 group-hover:text-white" />
|
||||
New Roadmap
|
||||
</button>
|
||||
</li>
|
||||
<li className="border-b border-b-gray-700/60 px-1 pb-1">
|
||||
<li className="px-1">
|
||||
<a
|
||||
href="/account/roadmaps"
|
||||
className="group flex items-center gap-2 rounded py-2 pl-3 pr-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
|
||||
className="block rounded pl-4 pr-2 py-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
|
||||
>
|
||||
<Map className="h-4 w-4 stroke-[2px] text-slate-400 group-hover:text-white" />
|
||||
Roadmaps
|
||||
</a>
|
||||
</li>
|
||||
<li className="px-1 pt-1">
|
||||
<li className="px-1">
|
||||
<button
|
||||
className="group flex w-full items-center justify-between rounded py-2 pl-3 pr-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
|
||||
className="group flex w-full items-center justify-between rounded pl-4 pr-2 py-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
|
||||
onClick={() => setIsTeamsOpen(true)}
|
||||
>
|
||||
<span className="flex items-center gap-2.5">
|
||||
<Users2 className="h-4 w-4 stroke-[2px] text-slate-400 group-hover:text-white" />
|
||||
Teams
|
||||
</span>
|
||||
Teams
|
||||
<ChevronRight className="h-4 w-4 shrink-0 stroke-[2.5px] text-slate-400 group-hover:text-white" />
|
||||
</button>
|
||||
</li>
|
||||
<li className="px-1">
|
||||
<button
|
||||
className="group flex gap-2 items-center w-full rounded py-2 pl-3 pr-2 text-left text-sm font-medium text-slate-100 hover:bg-slate-700"
|
||||
className="block w-full rounded pl-4 pr-2 py-2 text-left text-sm font-medium text-slate-100 hover:bg-slate-700"
|
||||
type="button"
|
||||
onClick={logout}
|
||||
>
|
||||
<LogOut className="h-4 w-4 stroke-[2px] text-slate-400 group-hover:text-white" />
|
||||
Logout
|
||||
</button>
|
||||
</li>
|
||||
|
||||
@@ -105,7 +105,7 @@ export function PageSponsor(props: PageSponsorProps) {
|
||||
</span>
|
||||
<img
|
||||
src={imageUrl}
|
||||
className="block h-[150px] object-fill lg:h-[169px] lg:w-[118.18px]"
|
||||
className="block h-[150px] w-[104.89px] object-contain lg:h-[169px] lg:w-[118.18px]"
|
||||
alt="Sponsor Banner"
|
||||
/>
|
||||
<span className="flex flex-1 flex-col justify-between text-sm">
|
||||
|
||||
@@ -23,7 +23,7 @@ function ProgressStatButton(props: ProgressStatButtonProps) {
|
||||
<button
|
||||
disabled={isDisabled}
|
||||
onClick={onClick}
|
||||
className="group relative flex flex-1 items-center overflow-hidden rounded-md border border-gray-300 bg-white px-2 py-2 text-sm text-black transition-colors hover:border-black disabled:pointer-events-none disabled:opacity-50 sm:rounded-xl sm:px-4 sm:py-3 sm:text-base"
|
||||
className="group relative text-sm sm:text-base flex flex-1 items-center overflow-hidden rounded-md sm:rounded-xl border border-gray-300 bg-white py-2 px-2 sm:py-3 sm:px-4 text-black transition-colors hover:border-black disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
{icon}
|
||||
<span className="flex flex-grow justify-between">
|
||||
@@ -31,7 +31,7 @@ function ProgressStatButton(props: ProgressStatButtonProps) {
|
||||
<span>{count}</span>
|
||||
</span>
|
||||
|
||||
<span className="absolute left-0 right-0 top-full flex h-full items-center justify-center border border-black bg-black text-white transition-all duration-200 group-hover:top-0">
|
||||
<span className="absolute top-full left-0 right-0 flex h-full items-center justify-center border border-black bg-black text-white transition-all duration-200 group-hover:top-0">
|
||||
Restart Asking
|
||||
</span>
|
||||
</button>
|
||||
@@ -62,7 +62,7 @@ export function QuestionFinished(props: QuestionFinishedProps) {
|
||||
<span className="inline sm:hidden">questions</span>
|
||||
</p>
|
||||
|
||||
<div className="mb-5 mt-5 flex w-full flex-col gap-1.5 px-2 sm:flex-row sm:gap-3 sm:px-16">
|
||||
<div className="mt-5 mb-5 flex w-full flex-col gap-1.5 sm:gap-3 px-2 sm:flex-row sm:px-16">
|
||||
<ProgressStatButton
|
||||
icon={<ThumbsUp className="mr-1 h-4" />}
|
||||
label="Knew"
|
||||
@@ -85,10 +85,10 @@ export function QuestionFinished(props: QuestionFinishedProps) {
|
||||
onClick={() => onReset('skip')}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4 mt-2 text-sm sm:mb-0">
|
||||
<div className="mt-2 mb-4 sm:mb-0 text-sm">
|
||||
<button
|
||||
onClick={() => onReset('reset')}
|
||||
className="flex items-center gap-0.5 text-sm text-red-700 hover:text-black sm:text-base"
|
||||
className="flex items-center gap-0.5 text-red-700 hover:text-black text-sm sm:text-base"
|
||||
>
|
||||
<RefreshCcw className="mr-1 h-4" />
|
||||
Restart Asking
|
||||
|
||||
@@ -46,7 +46,7 @@ export function QuestionsList(props: QuestionsListProps) {
|
||||
const { response, error } = await httpGet<UserQuestionProgress>(
|
||||
`${
|
||||
import.meta.env.PUBLIC_API_URL
|
||||
}/v1-get-user-question-progress/${groupId}`,
|
||||
}/v1-get-user-question-progress/${groupId}`
|
||||
);
|
||||
|
||||
if (error) {
|
||||
@@ -106,7 +106,7 @@ export function QuestionsList(props: QuestionsListProps) {
|
||||
}/v1-reset-question-progress/${groupId}`,
|
||||
{
|
||||
status: type,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
@@ -139,7 +139,7 @@ export function QuestionsList(props: QuestionsListProps) {
|
||||
|
||||
async function updateQuestionStatus(
|
||||
status: QuestionProgressType,
|
||||
questionId: string,
|
||||
questionId: string
|
||||
) {
|
||||
setIsLoading(true);
|
||||
let newProgress = userProgress || { know: [], dontKnow: [], skip: [] };
|
||||
@@ -161,7 +161,7 @@ export function QuestionsList(props: QuestionsListProps) {
|
||||
status,
|
||||
questionId,
|
||||
questionGroupId: groupId,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
@@ -173,7 +173,7 @@ export function QuestionsList(props: QuestionsListProps) {
|
||||
}
|
||||
|
||||
const updatedQuestionList = pendingQuestions.filter(
|
||||
(q) => q.id !== questionId,
|
||||
(q) => q.id !== questionId
|
||||
);
|
||||
|
||||
setUserProgress(newProgress);
|
||||
@@ -198,7 +198,7 @@ export function QuestionsList(props: QuestionsListProps) {
|
||||
const hasFinished = !isLoading && hasProgress && !currQuestion;
|
||||
|
||||
return (
|
||||
<div className="mb-0 gap-3 text-center sm:mb-40">
|
||||
<div className="mb-0 sm:mb-40 gap-3 text-center">
|
||||
<QuestionsProgress
|
||||
knowCount={knowCount}
|
||||
didNotKnowCount={dontKnowCount}
|
||||
@@ -241,7 +241,7 @@ export function QuestionsList(props: QuestionsListProps) {
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`flex flex-col gap-1 transition-opacity duration-300 sm:flex-row sm:gap-3 ${
|
||||
className={`flex flex-col gap-1 sm:gap-3 transition-opacity duration-300 sm:flex-row ${
|
||||
hasFinished ? 'opacity-0' : 'opacity-100'
|
||||
}`}
|
||||
>
|
||||
@@ -249,10 +249,10 @@ export function QuestionsList(props: QuestionsListProps) {
|
||||
disabled={isLoading || !currQuestion}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
updateQuestionStatus('know', currQuestion.id).finally(() => null);
|
||||
}}
|
||||
className="flex flex-1 items-center rounded-md border border-gray-300 bg-white px-2 py-2 text-sm text-black transition-colors hover:border-black hover:bg-black hover:text-white disabled:pointer-events-none disabled:opacity-50 sm:rounded-lg sm:px-4 sm:py-3 sm:text-base"
|
||||
className="flex flex-1 items-center rounded-md sm:rounded-lg border border-gray-300 bg-white text-sm sm:text-base py-2 px-2 sm:py-3 sm:px-4 text-black transition-colors hover:border-black hover:bg-black hover:text-white disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
<CheckCircle className="mr-1 h-4 text-current" />
|
||||
Already Know that
|
||||
@@ -260,11 +260,11 @@ export function QuestionsList(props: QuestionsListProps) {
|
||||
<button
|
||||
onClick={() => {
|
||||
updateQuestionStatus('dontKnow', currQuestion.id).finally(
|
||||
() => null,
|
||||
() => null
|
||||
);
|
||||
}}
|
||||
disabled={isLoading || !currQuestion}
|
||||
className="flex flex-1 items-center rounded-md border border-gray-300 bg-white px-2 py-2 text-sm text-black transition-colors hover:border-black hover:bg-black hover:text-white disabled:pointer-events-none disabled:opacity-50 sm:rounded-lg sm:px-4 sm:py-3 sm:text-base"
|
||||
className="flex flex-1 items-center rounded-md sm:rounded-lg border border-gray-300 bg-white text-sm sm:text-base py-2 px-2 sm:py-3 sm:px-4 text-black transition-colors hover:border-black hover:bg-black hover:text-white disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
<Sparkles className="mr-1 h-4 text-current" />
|
||||
Didn't Know that
|
||||
@@ -275,7 +275,7 @@ export function QuestionsList(props: QuestionsListProps) {
|
||||
}}
|
||||
disabled={isLoading || !currQuestion}
|
||||
data-next-question="skip"
|
||||
className="flex flex-1 items-center rounded-md border border-red-600 px-2 py-2 text-sm text-red-600 hover:bg-red-600 hover:text-white disabled:pointer-events-none disabled:opacity-50 sm:rounded-lg sm:px-4 sm:py-3 sm:text-base"
|
||||
className="flex flex-1 items-center rounded-md sm:rounded-lg border border-red-600 text-sm sm:text-base py-2 px-2 sm:py-3 sm:px-4 text-red-600 hover:bg-red-600 hover:text-white disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
<SkipForward className="mr-1 h-4" />
|
||||
Skip Question
|
||||
|
||||
@@ -26,7 +26,7 @@ export function QuestionsProgress(props: QuestionsProgressProps) {
|
||||
const donePercentage = (totalSolved / totalCount) * 100;
|
||||
|
||||
return (
|
||||
<div className="mb-3 overflow-hidden rounded-lg border border-gray-300 bg-white p-4 sm:mb-5 sm:p-6">
|
||||
<div className="mb-3 sm:mb-5 overflow-hidden rounded-lg border border-gray-300 bg-white p-4 sm:p-6">
|
||||
<div className="mb-3 flex items-center text-gray-600">
|
||||
<div className="relative w-full flex-1 rounded-xl bg-gray-200 p-1">
|
||||
<div
|
||||
@@ -79,12 +79,12 @@ export function QuestionsProgress(props: QuestionsProgressProps) {
|
||||
>
|
||||
<RotateCcw className="mr-1 h-4" />
|
||||
Reset
|
||||
<span className="inline lg:hidden">Progress</span>
|
||||
<span className='inline lg:hidden'>Progress</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showLoginAlert && (
|
||||
<p className="-mx-6 -mb-6 mt-6 border-t bg-yellow-100 py-3 text-sm text-yellow-900">
|
||||
<p className="-mx-6 mt-6 -mb-6 border-t bg-yellow-100 py-3 text-sm text-yellow-900">
|
||||
You progress is not saved. Please{' '}
|
||||
<button
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
---
|
||||
import { getQuestionGroupsByIds } from '../lib/question-group';
|
||||
import { getRoadmapsByIds, RoadmapFrontmatter } from '../lib/roadmap';
|
||||
import { Map, Clipboard } from 'lucide-react';
|
||||
|
||||
export interface Props {
|
||||
roadmap: RoadmapFrontmatter;
|
||||
@@ -10,89 +8,35 @@ export interface Props {
|
||||
const { roadmap } = Astro.props;
|
||||
|
||||
const relatedRoadmaps = roadmap.relatedRoadmaps || [];
|
||||
const relatedRoadmapDetails = await getRoadmapsByIds(relatedRoadmaps);
|
||||
if (!relatedRoadmaps.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const relatedQuestions = roadmap.relatedQuestions || [];
|
||||
const relatedQuestionDetails = await getQuestionGroupsByIds(relatedQuestions);
|
||||
const relatedRoadmapDetails = await getRoadmapsByIds(relatedRoadmaps);
|
||||
---
|
||||
|
||||
{
|
||||
relatedQuestionDetails.length > 0 && (
|
||||
<div class='border-t bg-gray-100 pb-3'>
|
||||
<div class='container'>
|
||||
<div class='relative -top-5 flex justify-between'>
|
||||
<span class='text-md flex items-center rounded-md border bg-white px-3 py-1 font-medium'>
|
||||
<Clipboard className='mr-1.5 text-black' size='17px' />
|
||||
Test your Knowledge
|
||||
<span class='ml-2 rounded-md border border-yellow-300 bg-yellow-100 px-1 py-0.5 text-xs uppercase'>
|
||||
New
|
||||
</span>
|
||||
</span>
|
||||
<a
|
||||
href='/roadmaps'
|
||||
class='text-md rounded-md border bg-white px-3 py-1 font-medium hover:bg-gray-50'
|
||||
>
|
||||
<span class='hidden sm:inline'>All Quizzes →</span>
|
||||
<span class='inline sm:hidden'>More →</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class='flex flex-col gap-1 pb-8'>
|
||||
{relatedQuestionDetails.map((relatedQuestionGroup) => (
|
||||
<a
|
||||
href={`/questions/${relatedQuestionGroup.id}`}
|
||||
class='flex flex-col gap-0.5 rounded-md border bg-white px-3.5 py-2 hover:bg-gray-50 sm:flex-row sm:gap-0'
|
||||
>
|
||||
<span class='inline-block min-w-[150px] font-medium'>
|
||||
{relatedQuestionGroup.title}
|
||||
</span>
|
||||
<span class='text-gray-500'>
|
||||
{relatedQuestionGroup.description}
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div class='border-t bg-gray-100'>
|
||||
<div class='container'>
|
||||
<div class='flex justify-between relative -top-5'>
|
||||
<span class='text-md font-medium py-1 px-3 border bg-white rounded-md'>Related Roadmaps</span>
|
||||
<a href='/roadmaps' class='text-md font-medium py-1 px-3 border bg-white rounded-md hover:bg-gray-50'>
|
||||
<span class='hidden sm:inline'>All Roadmaps →</span>
|
||||
<span class='inline sm:hidden'>More →</span>
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
relatedRoadmaps.length && (
|
||||
<div class:list={['border-t bg-gray-100', {
|
||||
'mt-8': !relatedQuestionDetails.length
|
||||
}]}>
|
||||
<div class='container'>
|
||||
<div class='relative -top-5 flex justify-between'>
|
||||
<span class='text-md flex items-center rounded-md border bg-white px-3 py-1 font-medium'>
|
||||
<Map className='text-black mr-1.5' size='17px' />
|
||||
Related Roadmaps
|
||||
</span>
|
||||
<div class='flex flex-col gap-1 pb-8'>
|
||||
{
|
||||
relatedRoadmapDetails.map((relatedRoadmap) => (
|
||||
<a
|
||||
href='/roadmaps'
|
||||
class='text-md rounded-md border bg-white px-3 py-1 font-medium hover:bg-gray-50'
|
||||
href={`/${relatedRoadmap.id}`}
|
||||
class='py-2 px-3.5 bg-white border rounded-md hover:bg-gray-50 flex flex-col sm:flex-row gap-0.5 sm:gap-0'
|
||||
>
|
||||
<span class='hidden sm:inline'>All Roadmaps →</span>
|
||||
<span class='inline sm:hidden'>More →</span>
|
||||
<span class='font-medium inline-block min-w-[150px]'>{relatedRoadmap.frontmatter.briefTitle}</span>
|
||||
<span class='text-gray-500'>{relatedRoadmap.frontmatter.briefDescription}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class='flex flex-col gap-1 pb-8'>
|
||||
{relatedRoadmapDetails.map((relatedRoadmap) => (
|
||||
<a
|
||||
href={`/${relatedRoadmap.id}`}
|
||||
class='flex flex-col gap-0.5 rounded-md border bg-white px-3.5 py-2 hover:bg-gray-50 sm:flex-row sm:gap-0'
|
||||
>
|
||||
<span class='inline-block min-w-[150px] font-medium'>
|
||||
{relatedRoadmap.frontmatter.briefTitle}
|
||||
</span>
|
||||
<span class='text-gray-500'>
|
||||
{relatedRoadmap.frontmatter.briefDescription}
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,8 +8,7 @@ import YouTubeAlert from './YouTubeAlert.astro';
|
||||
import ProgressHelpPopup from './ProgressHelpPopup.astro';
|
||||
import { MarkFavorite } from './FeaturedItems/MarkFavorite';
|
||||
import { TeamVersions } from './TeamVersions/TeamVersions';
|
||||
import { CreateVersion } from './CreateVersion/CreateVersion';
|
||||
import { type RoadmapFrontmatter } from '../lib/roadmap';
|
||||
import { RoadmapFrontmatter } from '../lib/roadmap';
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
@@ -21,7 +20,6 @@ export interface Props {
|
||||
hasSearch?: boolean;
|
||||
question?: RoadmapFrontmatter['question'];
|
||||
hasTopics?: boolean;
|
||||
isForkable?: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -34,7 +32,6 @@ const {
|
||||
note,
|
||||
hasTopics = false,
|
||||
question,
|
||||
isForkable = false,
|
||||
} = Astro.props;
|
||||
|
||||
const isRoadmapReady = !isUpcoming;
|
||||
@@ -61,21 +58,13 @@ const hasTnsBanner = !!tnsBannerLink;
|
||||
]}
|
||||
>
|
||||
<div class='mb-3 mt-0 sm:mb-4'>
|
||||
{
|
||||
isForkable && (
|
||||
<div class='mb-2'>
|
||||
<CreateVersion client:load roadmapId={roadmapId} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<h1 class='mb-0.5 text-2xl font-bold sm:mb-2 sm:text-4xl'>
|
||||
{title}
|
||||
<span class='relative top-0 sm:-top-1'>
|
||||
<MarkFavorite
|
||||
resourceId={roadmapId}
|
||||
resourceType='roadmap'
|
||||
className='relative ml-1.5 text-gray-500 !opacity-100 hover:text-gray-600 focus:outline-0 [&>svg]:h-4 [&>svg]:w-4 [&>svg]:stroke-gray-400 [&>svg]:stroke-[0.4] hover:[&>svg]:stroke-gray-600 sm:[&>svg]:h-4 sm:[&>svg]:w-4'
|
||||
className='text-gray-500 !opacity-100 hover:text-gray-600 [&>svg]:stroke-[0.4] [&>svg]:stroke-gray-400 hover:[&>svg]:stroke-gray-600 [&>svg]:h-4 [&>svg]:w-4 sm:[&>svg]:h-4 sm:[&>svg]:w-4 ml-1.5 relative focus:outline-0'
|
||||
client:only='react'
|
||||
/>
|
||||
</span>
|
||||
|
||||
@@ -24,14 +24,14 @@ export function RoadmapTitleQuestion(props: RoadmapTitleQuestionProps) {
|
||||
<div className="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50"></div>
|
||||
)}
|
||||
<h2
|
||||
className="z-50 flex cursor-pointer items-center px-2 py-2.5 text-base font-medium"
|
||||
className="z-50 flex cursor-pointer items-center px-2 py-2.5 font-medium text-base"
|
||||
aria-expanded={isAnswerVisible ? 'true' : 'false'}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setIsAnswerVisible(!isAnswerVisible);
|
||||
}}
|
||||
>
|
||||
<span className="flex flex-grow items-center">
|
||||
<span className="flex items-center flex-grow">
|
||||
<GraduationCap className="mr-2 inline-block h-6 w-6" />
|
||||
{question}
|
||||
</span>
|
||||
@@ -61,7 +61,7 @@ export function RoadmapTitleQuestion(props: RoadmapTitleQuestionProps) {
|
||||
</h2>
|
||||
)}
|
||||
<div
|
||||
className="bg-gray-100 p-3 text-base [&>h2]:mb-2 [&>h2]:mt-5 [&>h2]:text-[17px] [&>h2]:font-medium [&>p:last-child]:mb-0 [&>p>a]:font-semibold [&>p>a]:underline [&>p>a]:underline-offset-2 [&>p]:mb-3 [&>p]:font-normal [&>p]:leading-relaxed [&>p]:text-gray-800"
|
||||
className="bg-gray-100 [&>p]:text-gray-800 p-3 text-base [&>h2]:mb-2 [&>h2]:mt-5 [&>h2]:text-[17px] [&>h2]:font-medium [&>p:last-child]:mb-0 [&>p>a]:font-semibold [&>p>a]:underline [&>p>a]:underline-offset-2 [&>p]:mb-3 [&>p]:font-normal [&>p]:leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: markdownToHtml(answer, false) }}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { UserItem } from './UserItem';
|
||||
import { Check, Copy, Group, UserPlus2, Users2 } from 'lucide-react';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { getUser } from '../../lib/jwt.ts';
|
||||
import { useCopyText } from '../../hooks/use-copy-text.ts';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import { Users2 } from 'lucide-react';
|
||||
import {httpGet} from "../../lib/http";
|
||||
|
||||
export type FriendshipStatus =
|
||||
| 'none'
|
||||
@@ -44,13 +41,10 @@ type ShareFriendListProps = {
|
||||
};
|
||||
|
||||
export function ShareFriendList(props: ShareFriendListProps) {
|
||||
const userId = getUser()?.id!;
|
||||
const { setFriends, friends, sharedFriendIds, setSharedFriendIds } = props;
|
||||
const toast = useToast();
|
||||
|
||||
const { isCopied, copyText } = useCopyText();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isAddingFriend, setIsAddingFriend] = useState(false);
|
||||
|
||||
async function loadFriends() {
|
||||
if (friends.length > 0) {
|
||||
@@ -59,7 +53,7 @@ export function ShareFriendList(props: ShareFriendListProps) {
|
||||
|
||||
setIsLoading(true);
|
||||
const { response, error } = await httpGet<ListFriendsResponse>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-list-friends`,
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-list-friends`
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
@@ -93,10 +87,6 @@ export function ShareFriendList(props: ShareFriendListProps) {
|
||||
</ul>
|
||||
);
|
||||
|
||||
const isDev = import.meta.env.DEV;
|
||||
const baseWebUrl = isDev ? 'http://localhost:3000' : 'https://roadmap.sh';
|
||||
const befriendUrl = `${baseWebUrl}/befriend?u=${userId}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
{(friends.length > 0 || isLoading) && (
|
||||
@@ -122,85 +112,32 @@ export function ShareFriendList(props: ShareFriendListProps) {
|
||||
|
||||
{loadingFriends}
|
||||
{friends.length > 0 && !isLoading && (
|
||||
<>
|
||||
<ul className="mt-2 grid grid-cols-3 gap-1.5">
|
||||
{friends.map((friend) => {
|
||||
const isSelected = sharedFriendIds?.includes(friend.userId);
|
||||
return (
|
||||
<li key={friend.userId}>
|
||||
<UserItem
|
||||
user={{
|
||||
name: friend.name,
|
||||
avatar: friend.avatar,
|
||||
email: friend.email,
|
||||
}}
|
||||
isSelected={isSelected}
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
setSharedFriendIds(
|
||||
sharedFriendIds.filter((id) => id !== friend.userId),
|
||||
);
|
||||
} else {
|
||||
setSharedFriendIds([...sharedFriendIds, friend.userId]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{!isAddingFriend && (
|
||||
<p className="mt-6 text-sm text-gray-600">
|
||||
Don't see a Friend?{' '}
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsAddingFriend(true);
|
||||
}}
|
||||
className="font-semibold text-gray-900 underline"
|
||||
>
|
||||
Add them
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
{isAddingFriend && (
|
||||
<div className="-mx-4 -mb-4 mt-6 border-t bg-gray-50 px-4 py-4">
|
||||
<p className="mb-1.5 flex items-center gap-1 text-sm text-gray-800">
|
||||
<UserPlus2 className="text-gray-500" size="20px" />
|
||||
Share the link below with your friends to invite them
|
||||
</p>
|
||||
<div className="relative">
|
||||
<input
|
||||
readOnly
|
||||
type="text"
|
||||
value={befriendUrl}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
(e.target as HTMLInputElement).select();
|
||||
copyText(befriendUrl);
|
||||
<ul className="mt-2 grid grid-cols-3 gap-1.5">
|
||||
{friends.map((friend) => {
|
||||
const isSelected = sharedFriendIds?.includes(friend.userId);
|
||||
return (
|
||||
<li key={friend.userId}>
|
||||
<UserItem
|
||||
user={{
|
||||
name: friend.name,
|
||||
avatar: friend.avatar,
|
||||
email: friend.email,
|
||||
}}
|
||||
isSelected={isSelected}
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
setSharedFriendIds(
|
||||
sharedFriendIds.filter((id) => id !== friend.userId)
|
||||
);
|
||||
} else {
|
||||
setSharedFriendIds([...sharedFriendIds, friend.userId]);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'w-full rounded-md border px-2 py-2 text-sm focus:shadow-none focus:outline-0',
|
||||
{
|
||||
'border-green-400 bg-green-50': isCopied,
|
||||
},
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
onClick={() => copyText(befriendUrl)}
|
||||
className="absolute bottom-0 right-0 top-0 flex items-center px-2.5"
|
||||
>
|
||||
{isCopied ? (
|
||||
<span className="flex items-center gap-1 text-sm font-medium text-green-600">
|
||||
<Check className="text-green-600" size="18px" /> Copied
|
||||
</span>
|
||||
) : (
|
||||
<Copy className="text-gray-400" size="18px" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{friends.length === 0 && !isLoading && (
|
||||
@@ -211,7 +148,7 @@ export function ShareFriendList(props: ShareFriendListProps) {
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
href={`/account/friends`}
|
||||
href={`${import.meta.env.PUBLIC_ROADMAP_WEB_URL}/account/friends`}
|
||||
>
|
||||
Invite your friends to share roadmaps with.
|
||||
</a>
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useState,
|
||||
useMemo,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import { type ReactNode, useCallback, useState, useMemo } from 'react';
|
||||
import { Globe2, Loader2, Lock } from 'lucide-react';
|
||||
import { type ListFriendsResponse, ShareFriendList } from './ShareFriendList';
|
||||
import { TransferToTeamList } from './TransferToTeamList';
|
||||
@@ -22,7 +16,6 @@ import { cn } from '../../lib/classname';
|
||||
import type { UserTeamItem } from '../TeamDropdown/TeamDropdown';
|
||||
|
||||
export type OnShareSettingsUpdate = (options: {
|
||||
isDiscoverable: boolean;
|
||||
visibility: AllowedRoadmapVisibility;
|
||||
sharedTeamMemberIds: string[];
|
||||
sharedFriendIds: string[];
|
||||
@@ -31,7 +24,6 @@ export type OnShareSettingsUpdate = (options: {
|
||||
type ShareOptionsModalProps = {
|
||||
onClose: () => void;
|
||||
visibility: AllowedRoadmapVisibility;
|
||||
isDiscoverable?: boolean;
|
||||
sharedFriendIds?: string[];
|
||||
sharedTeamMemberIds?: string[];
|
||||
teamId?: string;
|
||||
@@ -45,7 +37,6 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
|
||||
const {
|
||||
roadmapId,
|
||||
onClose,
|
||||
isDiscoverable: defaultIsDiscoverable = false,
|
||||
visibility: defaultVisibility,
|
||||
sharedTeamMemberIds: defaultSharedMemberIds = [],
|
||||
sharedFriendIds: defaultSharedFriendIds = [],
|
||||
@@ -66,7 +57,7 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
|
||||
const membersCache = useMemo(() => new Map<string, TeamMemberList[]>(), []);
|
||||
|
||||
const [visibility, setVisibility] = useState(defaultVisibility);
|
||||
const [isDiscoverable, setIsDiscoverable] = useState(defaultIsDiscoverable);
|
||||
const [isDiscoverable, setIsDiscoverable] = useState(false);
|
||||
const [sharedTeamMemberIds, setSharedTeamMemberIds] = useState<string[]>(
|
||||
defaultSharedMemberIds
|
||||
);
|
||||
@@ -130,12 +121,7 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
|
||||
|
||||
setIsLoading(false);
|
||||
setIsSettingsUpdated(true);
|
||||
onShareSettingsUpdate({
|
||||
isDiscoverable,
|
||||
sharedFriendIds,
|
||||
visibility,
|
||||
sharedTeamMemberIds,
|
||||
});
|
||||
onShareSettingsUpdate({ sharedFriendIds, visibility, sharedTeamMemberIds });
|
||||
};
|
||||
|
||||
const handleTransferToTeam = useCallback(
|
||||
@@ -223,8 +209,6 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
|
||||
setSharedFriendIds([]);
|
||||
setSharedTeamMemberIds([]);
|
||||
}
|
||||
|
||||
setIsDiscoverable(visibility === 'public');
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -343,7 +327,6 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
|
||||
disabled={isUpdateDisabled || isLoading}
|
||||
onClick={() => {
|
||||
handleShareChange({
|
||||
isDiscoverable,
|
||||
visibility,
|
||||
sharedTeamMemberIds:
|
||||
visibility === 'team' ? sharedTeamMemberIds : [],
|
||||
|
||||
@@ -8,17 +8,10 @@ type ShareSuccessProps = {
|
||||
onClose: () => void;
|
||||
visibility: AllowedRoadmapVisibility;
|
||||
description?: string;
|
||||
isSharingWithOthers?: boolean;
|
||||
};
|
||||
|
||||
export function ShareSuccess(props: ShareSuccessProps) {
|
||||
const {
|
||||
roadmapId,
|
||||
onClose,
|
||||
description,
|
||||
visibility,
|
||||
isSharingWithOthers = false,
|
||||
} = props;
|
||||
const { roadmapId, onClose, description, visibility } = props;
|
||||
|
||||
const baseUrl = import.meta.env.DEV
|
||||
? 'http://localhost:3000'
|
||||
@@ -49,11 +42,7 @@ export function ShareSuccess(props: ShareSuccessProps) {
|
||||
<div className="flex grow flex-col justify-center">
|
||||
<div className="mt-5 flex grow flex-col items-center justify-center gap-1.5">
|
||||
<CheckCircle className="h-14 w-14 text-green-500" />
|
||||
{isSharingWithOthers ? (
|
||||
<h3 className="text-xl font-medium">Sharing with Others</h3>
|
||||
) : (
|
||||
<h3 className="text-xl font-medium">Sharing Settings Updated</h3>
|
||||
)}
|
||||
<h3 className="text-xl font-medium">Sharing Settings Updated</h3>
|
||||
</div>
|
||||
|
||||
<input
|
||||
@@ -66,26 +55,20 @@ export function ShareSuccess(props: ShareSuccessProps) {
|
||||
copyText(shareLink);
|
||||
}}
|
||||
/>
|
||||
{isSharingWithOthers ? (
|
||||
<p className="mt-1 text-sm text-gray-400">
|
||||
You can share the above link with anyone
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-1 text-sm text-gray-400">
|
||||
You can share the above link with anyone who has access
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-1 text-sm text-gray-400">
|
||||
You can share the above link with anyone who has access
|
||||
</p>
|
||||
|
||||
{visibility === 'public' && (
|
||||
<>
|
||||
<div className="-mx-4 mt-4 flex items-center gap-1.5">
|
||||
<span className="h-px grow bg-gray-300" />
|
||||
<span className="px-2 text-xs uppercase text-gray-400">Or</span>
|
||||
<span className="text-sm uppercase text-gray-600">Or</span>
|
||||
<span className="h-px grow bg-gray-300" />
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600">Share with others on</span>
|
||||
<span className="text-sm text-gray-600">Share on</span>
|
||||
<ul className="flex items-center gap-1.5">
|
||||
{socialShareLinks.map((socialShareLink) => (
|
||||
<li key={socialShareLink.title}>
|
||||
@@ -116,7 +99,7 @@ export function ShareSuccess(props: ShareSuccessProps) {
|
||||
}}
|
||||
>
|
||||
<Copy className="h-3.5 w-3.5 stroke-[2.5]" />
|
||||
{isCopied ? 'Copied' : 'Copy URL'}
|
||||
{isCopied ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
<button
|
||||
className={cn(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CheckCircle } from 'lucide-react';
|
||||
import { CheckCircle, CheckCircle2, CheckIcon } from 'lucide-react';
|
||||
import { isLoggedIn } from '../../lib/jwt.ts';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Copy } from 'lucide-react';
|
||||
import { Check, CheckCircle, Copy, Sparkles } from 'lucide-react';
|
||||
import { useCopyText } from '../../hooks/use-copy-text.ts';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import { isLoggedIn } from '../../lib/jwt.ts';
|
||||
@@ -76,7 +76,7 @@ export function TeamPricing() {
|
||||
copyText(teamEmail);
|
||||
}}
|
||||
className={cn(
|
||||
'relative flex items-center justify-between gap-3 overflow-hidden rounded-md border border-black bg-white px-4 py-2 text-black hover:bg-gray-100',
|
||||
'relative flex items-center justify-between gap-3 overflow-hidden rounded-md border border-black bg-white px-4 py-2 text-black hover:bg-gray-100'
|
||||
)}
|
||||
>
|
||||
{teamEmail}
|
||||
@@ -91,7 +91,7 @@ export function TeamPricing() {
|
||||
{
|
||||
'top-full': !isCopied,
|
||||
'top-0': isCopied,
|
||||
},
|
||||
}
|
||||
)}
|
||||
>
|
||||
Email copied!
|
||||
|
||||
@@ -7,7 +7,7 @@ const toolsList = [
|
||||
{
|
||||
imageUrl: '/images/team-promo/progress-tracking.png',
|
||||
title: 'Progress tracking',
|
||||
description: 'Track and compare the progress of team members.',
|
||||
description: 'Track the and compare the progress of team members.',
|
||||
},
|
||||
{
|
||||
imageUrl: '/images/team-promo/onboarding.png',
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
type MouseEvent,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { Spinner } from '../ReactIcons/Spinner';
|
||||
import '../FrameRenderer/FrameRenderer.css';
|
||||
import type { TeamMember } from './TeamProgressPage';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import {
|
||||
renderTopicProgress,
|
||||
type ResourceProgressType,
|
||||
type ResourceType,
|
||||
updateResourceProgress,
|
||||
} from '../../lib/resource-progress';
|
||||
import CloseIcon from '../../icons/close.svg';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import type { GetRoadmapResponse } from '../CustomRoadmap/CustomRoadmap';
|
||||
import { ReadonlyEditor } from '../../../editor/readonly-editor';
|
||||
import type { Node } from 'reactflow';
|
||||
import { useKeydown } from '../../hooks/use-keydown';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { MemberProgressModalHeader } from './MemberProgressModalHeader';
|
||||
|
||||
export type ProgressMapProps = {
|
||||
member: TeamMember;
|
||||
teamId: string;
|
||||
resourceId: string;
|
||||
resourceType: 'roadmap' | 'best-practice';
|
||||
onClose: () => void;
|
||||
onShowMyProgress: () => void;
|
||||
isCustomResource?: boolean;
|
||||
};
|
||||
|
||||
export type MemberProgressResponse = {
|
||||
removed: string[];
|
||||
done: string[];
|
||||
learning: string[];
|
||||
skipped: string[];
|
||||
};
|
||||
|
||||
export function MemberCustomProgressModal(props: ProgressMapProps) {
|
||||
const {
|
||||
resourceId,
|
||||
member,
|
||||
resourceType,
|
||||
onShowMyProgress,
|
||||
teamId,
|
||||
onClose,
|
||||
} = props;
|
||||
|
||||
const user = useAuth();
|
||||
const isCurrentUser = user?.email === member.email;
|
||||
|
||||
const popupBodyEl = useRef<HTMLDivElement>(null);
|
||||
const [roadmap, setRoadmap] = useState<GetRoadmapResponse | null>(null);
|
||||
const [memberProgress, setMemberProgress] =
|
||||
useState<MemberProgressResponse>();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const toast = useToast();
|
||||
|
||||
useKeydown('Escape', () => onClose());
|
||||
useOutsideClick(popupBodyEl, () => onClose());
|
||||
|
||||
async function getMemberProgress(
|
||||
teamId: string,
|
||||
memberId: string,
|
||||
resourceType: string,
|
||||
resourceId: string,
|
||||
) {
|
||||
const { error, response } = await httpGet<MemberProgressResponse>(
|
||||
`${
|
||||
import.meta.env.PUBLIC_API_URL
|
||||
}/v1-get-member-resource-progress/${teamId}/${memberId}?resourceType=${resourceType}&resourceId=${resourceId}`,
|
||||
);
|
||||
if (error || !response) {
|
||||
toast.error(error?.message || 'Failed to get member progress');
|
||||
return;
|
||||
}
|
||||
|
||||
setMemberProgress(response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function getRoadmap() {
|
||||
const { response, error } = await httpGet<GetRoadmapResponse>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-roadmap/${resourceId}`,
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
toast.error(error?.message || 'Failed to load roadmap');
|
||||
return;
|
||||
}
|
||||
|
||||
setRoadmap(response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!resourceId || !resourceType || !teamId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
Promise.all([
|
||||
getRoadmap(),
|
||||
getMemberProgress(teamId, member._id, resourceType, resourceId),
|
||||
])
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
toast.error(err?.message || 'Something went wrong. Please try again!');
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [member]);
|
||||
|
||||
function updateTopicStatus(topicId: string, newStatus: ResourceProgressType) {
|
||||
if (!resourceId || !resourceType || !isCurrentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
pageProgressMessage.set('Updating progress');
|
||||
updateResourceProgress(
|
||||
{
|
||||
resourceId: resourceId,
|
||||
resourceType: resourceType as ResourceType,
|
||||
topicId,
|
||||
},
|
||||
newStatus,
|
||||
)
|
||||
.then(() => {
|
||||
renderTopicProgress(topicId, newStatus);
|
||||
getMemberProgress(teamId, member._id, resourceType, resourceId).then(
|
||||
(data) => {
|
||||
setMemberProgress(data);
|
||||
},
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
alert('Something went wrong, please try again.');
|
||||
console.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const handleTopicRightClick = useCallback((e: MouseEvent, node: Node) => {
|
||||
if (!isCurrentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = e?.currentTarget as HTMLDivElement;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentStatusDone = target?.classList.contains('done');
|
||||
updateTopicStatus(node.id, isCurrentStatusDone ? 'pending' : 'done');
|
||||
}, []);
|
||||
|
||||
const handleTopicShiftClick = useCallback((e: MouseEvent, node: Node) => {
|
||||
if (!isCurrentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = e?.currentTarget as HTMLDivElement;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentStatusLearning = target?.classList.contains('learning');
|
||||
updateTopicStatus(
|
||||
node.id,
|
||||
isCurrentStatusLearning ? 'pending' : 'learning',
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleTopicAltClick = useCallback((e: MouseEvent, node: Node) => {
|
||||
if (!isCurrentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = e?.currentTarget as HTMLDivElement;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentStatusSkipped = target?.classList.contains('skipped');
|
||||
updateTopicStatus(node.id, isCurrentStatusSkipped ? 'pending' : 'skipped');
|
||||
}, []);
|
||||
|
||||
const handleLinkClick = useCallback((linkId: string, href: string) => {
|
||||
if (!href || !isCurrentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isExternalLink = href.startsWith('http');
|
||||
if (isExternalLink) {
|
||||
window.open(href, '_blank');
|
||||
} else {
|
||||
window.location.href = href;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50">
|
||||
<div
|
||||
id="original-roadmap"
|
||||
className="relative mx-auto h-full w-full max-w-4xl p-4 md:h-auto"
|
||||
>
|
||||
<div
|
||||
className="relative rounded-lg bg-white pt-[1px] shadow"
|
||||
ref={popupBodyEl}
|
||||
>
|
||||
<MemberProgressModalHeader
|
||||
resourceId={resourceId}
|
||||
member={member}
|
||||
progress={memberProgress}
|
||||
isCurrentUser={isCurrentUser}
|
||||
onShowMyProgress={onShowMyProgress}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
{!isLoading && roadmap && (
|
||||
<div className="px-4 pb-2">
|
||||
<ReadonlyEditor
|
||||
variant="modal"
|
||||
roadmap={roadmap!}
|
||||
className="min-h-[400px]"
|
||||
onRendered={() => {
|
||||
const {
|
||||
removed = [],
|
||||
done = [],
|
||||
learning = [],
|
||||
skipped = [],
|
||||
} = memberProgress || {};
|
||||
|
||||
done.forEach((id: string) => renderTopicProgress(id, 'done'));
|
||||
learning.forEach((id: string) =>
|
||||
renderTopicProgress(id, 'learning'),
|
||||
);
|
||||
skipped.forEach((id: string) =>
|
||||
renderTopicProgress(id, 'skipped'),
|
||||
);
|
||||
removed.forEach((id: string) =>
|
||||
renderTopicProgress(id, 'removed'),
|
||||
);
|
||||
}}
|
||||
onTopicRightClick={handleTopicRightClick}
|
||||
onTopicShiftClick={handleTopicShiftClick}
|
||||
onTopicAltClick={handleTopicAltClick}
|
||||
onButtonNodeClick={handleLinkClick}
|
||||
onLinkClick={handleLinkClick}
|
||||
fontFamily="Balsamiq Sans"
|
||||
fontURL="/fonts/balsamiq.woff2"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex w-full justify-center">
|
||||
<Spinner
|
||||
isDualRing={false}
|
||||
className="mb-4 mt-2 h-4 w-4 animate-spin fill-blue-600 text-gray-200 sm:h-8 sm:w-8"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`absolute right-2.5 top-3 z-50 ml-auto inline-flex items-center rounded-lg bg-transparent p-1.5 text-sm text-gray-400 hover:text-gray-900 lg:hidden ${
|
||||
isCurrentUser ? 'hover:bg-gray-800' : 'hover:bg-gray-100'
|
||||
}`}
|
||||
onClick={onClose}
|
||||
>
|
||||
<img alt={'close'} src={CloseIcon.src} className="h-4 w-4" />
|
||||
<span className="sr-only">Close modal</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -16,8 +16,13 @@ import CloseIcon from '../../icons/close.svg';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import { MemberProgressModalHeader } from './MemberProgressModalHeader';
|
||||
import {replaceChildren} from "../../lib/dom.ts";
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $currentTeam } from '../../stores/team';
|
||||
import { renderFlowJSON } from '../../../renderer/renderer';
|
||||
import {
|
||||
allowedClickableNodeTypes,
|
||||
getNodeDetails,
|
||||
} from '../CustomRoadmap/RoadmapRenderer';
|
||||
|
||||
export type ProgressMapProps = {
|
||||
member: TeamMember;
|
||||
@@ -44,6 +49,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
onShowMyProgress,
|
||||
teamId,
|
||||
onClose,
|
||||
isCustomResource,
|
||||
} = props;
|
||||
const user = useAuth();
|
||||
const isCurrentUser = user?.email === member.email;
|
||||
@@ -64,6 +70,12 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
resourceJsonUrl += `/best-practices/${resourceId}.json`;
|
||||
}
|
||||
|
||||
if (isCustomResource) {
|
||||
resourceJsonUrl = `${
|
||||
import.meta.env.PUBLIC_API_URL
|
||||
}/v1-get-roadmap/${resourceId}`;
|
||||
}
|
||||
|
||||
async function getMemberProgress(
|
||||
teamId: string,
|
||||
memberId: string,
|
||||
@@ -86,14 +98,30 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
}
|
||||
|
||||
async function renderResource(jsonUrl: string) {
|
||||
const res = await fetch(jsonUrl, {});
|
||||
const json = await res.json();
|
||||
const svg: SVGElement | null = await wireframeJSONToSVG(json, {
|
||||
fontURL: '/fonts/balsamiq.woff2',
|
||||
const res = await fetch(jsonUrl, {
|
||||
...(isCustomResource && {
|
||||
credentials: 'include',
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
let svg: SVGElement | null = null;
|
||||
if (isCustomResource) {
|
||||
svg = await renderFlowJSON(
|
||||
{
|
||||
nodes: json.nodes,
|
||||
edges: json.edges,
|
||||
},
|
||||
{
|
||||
fontURL: '/fonts/balsamiq.woff2',
|
||||
}
|
||||
);
|
||||
} else {
|
||||
svg = await wireframeJSONToSVG(json, {
|
||||
fontURL: '/fonts/balsamiq.woff2',
|
||||
});
|
||||
}
|
||||
|
||||
replaceChildren(containerEl.current!, svg);
|
||||
// containerEl.current?.replaceChildren(svg);
|
||||
containerEl.current?.replaceChildren(svg);
|
||||
}
|
||||
|
||||
useKeydown('Escape', () => {
|
||||
@@ -187,11 +215,29 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : '';
|
||||
if (!groupId) {
|
||||
return;
|
||||
let topicId = '';
|
||||
if (isCustomResource) {
|
||||
const { nodeId, nodeType } = getNodeDetails(e.target as SVGElement) || {};
|
||||
if (
|
||||
!nodeId ||
|
||||
!nodeType ||
|
||||
!allowedClickableNodeTypes.includes(nodeType)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nodeType === 'button') {
|
||||
return;
|
||||
}
|
||||
|
||||
topicId = nodeId;
|
||||
} else {
|
||||
const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : '';
|
||||
if (!groupId) {
|
||||
return;
|
||||
}
|
||||
topicId = groupId.replace(/^\d+-/, '');
|
||||
}
|
||||
const topicId = groupId.replace(/^\d+-/, '');
|
||||
|
||||
if (targetGroup.classList.contains('removed')) {
|
||||
e.preventDefault();
|
||||
@@ -209,11 +255,29 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
if (!targetGroup) {
|
||||
return;
|
||||
}
|
||||
const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : '';
|
||||
if (!groupId) {
|
||||
return;
|
||||
let topicId = '';
|
||||
if (isCustomResource) {
|
||||
const { nodeId, nodeType } = getNodeDetails(e.target as SVGElement) || {};
|
||||
if (
|
||||
!nodeId ||
|
||||
!nodeType ||
|
||||
!allowedClickableNodeTypes.includes(nodeType)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nodeType === 'button') {
|
||||
return;
|
||||
}
|
||||
|
||||
topicId = nodeId;
|
||||
} else {
|
||||
const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : '';
|
||||
if (!groupId) {
|
||||
return;
|
||||
}
|
||||
topicId = groupId.replace(/^\d+-/, '');
|
||||
}
|
||||
const topicId = groupId.replace(/^\d+-/, '');
|
||||
|
||||
if (targetGroup.classList.contains('removed')) {
|
||||
return;
|
||||
@@ -257,24 +321,136 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
};
|
||||
}, [member]);
|
||||
|
||||
const removedTopics = memberProgress?.removed || [];
|
||||
const memberDone =
|
||||
memberProgress?.done.filter((id) => !removedTopics.includes(id)).length ||
|
||||
0;
|
||||
const memberLearning =
|
||||
memberProgress?.learning.filter((id) => !removedTopics.includes(id))
|
||||
.length || 0;
|
||||
const memberSkipped =
|
||||
memberProgress?.skipped.filter((id) => !removedTopics.includes(id))
|
||||
.length || 0;
|
||||
|
||||
const currProgress = member.progress.find((p) => p.resourceId === resourceId);
|
||||
const memberTotal = currProgress?.total || 0;
|
||||
|
||||
const progressPercentage = Math.round((memberDone / memberTotal) * 100);
|
||||
|
||||
return (
|
||||
<div className="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50">
|
||||
<div
|
||||
id={'customized-roadmap'}
|
||||
id={isCustomResource ? 'original-roadmap' : 'customized-roadmap'}
|
||||
className="relative mx-auto h-full w-full max-w-4xl p-4 md:h-auto"
|
||||
>
|
||||
<div
|
||||
ref={popupBodyEl}
|
||||
className="popup-body relative rounded-lg bg-white pt-[1px] shadow"
|
||||
>
|
||||
<MemberProgressModalHeader
|
||||
resourceId={resourceId}
|
||||
member={member}
|
||||
progress={memberProgress}
|
||||
isCurrentUser={isCurrentUser}
|
||||
onShowMyProgress={onShowMyProgress}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
{isCurrentUser && (
|
||||
<div className="sticky top-1 mx-1 mb-0 mt-1 rounded-xl bg-gray-900 p-4 text-gray-300">
|
||||
<h2 className={'mb-1.5 text-base'}>
|
||||
Follow the Instructions below to update your progress
|
||||
</h2>
|
||||
<ul className="flex flex-col gap-1">
|
||||
<li className="leading-loose">
|
||||
<kbd className="rounded-md bg-yellow-200 px-2 py-1.5 text-xs text-gray-900">
|
||||
Right Mouse Click
|
||||
</kbd>{' '}
|
||||
on a topic to mark as{' '}
|
||||
<span className={'font-medium text-white'}>Done</span>.
|
||||
</li>
|
||||
<li className="leading-loose">
|
||||
<kbd className="rounded-md bg-yellow-200 px-2 py-1.5 text-xs text-gray-900">
|
||||
Shift
|
||||
</kbd>{' '}
|
||||
+{' '}
|
||||
<kbd className="rounded-md bg-yellow-200 px-2 py-1.5 text-xs text-gray-900">
|
||||
Click
|
||||
</kbd>{' '}
|
||||
on a topic to mark as{' '}
|
||||
<span className="font-medium text-white">In progress</span>.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4">
|
||||
{!isCurrentUser && (
|
||||
<div className="mb-5 mt-0 text-left md:mt-4 md:text-center">
|
||||
<h2 className={'mb-1 text-lg font-bold md:text-2xl'}>
|
||||
{member.name}'s Progress
|
||||
</h2>
|
||||
<p
|
||||
className={
|
||||
'hidden text-xs text-gray-500 sm:text-sm md:block md:text-base'
|
||||
}
|
||||
>
|
||||
You are looking at {member.name}'s progress.{' '}
|
||||
<button
|
||||
className="text-blue-600 underline"
|
||||
onClick={onShowMyProgress}
|
||||
>
|
||||
View your progress
|
||||
</button>
|
||||
.
|
||||
</p>
|
||||
<p className={'block text-gray-500 md:hidden'}>
|
||||
<button
|
||||
className="text-blue-600 underline"
|
||||
onClick={onShowMyProgress}
|
||||
>
|
||||
View your progress.
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<p
|
||||
className={`-mx-4 mb-3 flex items-center justify-start border-b border-t px-4 py-2 text-sm sm:hidden ${
|
||||
isLoading ? 'striped-loader' : ''
|
||||
}`}
|
||||
>
|
||||
<span className="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900">
|
||||
<span>{progressPercentage}</span>% Done
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<span>{memberDone}</span> of <span>{memberTotal}</span> done
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
className={`-mx-4 mb-3 hidden items-center justify-center border-b border-t py-2 text-sm sm:flex ${
|
||||
isLoading ? 'striped-loader' : ''
|
||||
}`}
|
||||
>
|
||||
<span className="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900">
|
||||
<span>{progressPercentage}</span>% Done
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<span>{memberDone}</span> completed
|
||||
</span>
|
||||
<span className="mx-1.5 text-gray-400">·</span>
|
||||
<span>
|
||||
<span data-progress-learning="">{memberLearning}</span> in
|
||||
progress
|
||||
</span>
|
||||
|
||||
{memberSkipped > 0 && (
|
||||
<>
|
||||
<span className="mx-1.5 text-gray-400">·</span>
|
||||
<span>
|
||||
<span data-progress-skipped="">{memberSkipped}</span>{' '}
|
||||
skipped
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
<span className="mx-1.5 text-gray-400">·</span>
|
||||
<span>
|
||||
<span data-progress-total="">{memberTotal}</span> Total
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id={'resource-svg-wrap'}
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
import type { MemberProgressResponse } from './MemberCustomProgressModal';
|
||||
import type { TeamMember } from './TeamProgressPage';
|
||||
|
||||
type MemberProgressModalHeaderProps = {
|
||||
member: TeamMember;
|
||||
progress?: MemberProgressResponse;
|
||||
resourceId: string;
|
||||
isLoading: boolean;
|
||||
onShowMyProgress: () => void;
|
||||
isCurrentUser: boolean;
|
||||
};
|
||||
|
||||
export function MemberProgressModalHeader(
|
||||
props: MemberProgressModalHeaderProps
|
||||
) {
|
||||
const {
|
||||
progress: memberProgress,
|
||||
member,
|
||||
resourceId,
|
||||
isLoading,
|
||||
onShowMyProgress,
|
||||
isCurrentUser,
|
||||
} = props;
|
||||
|
||||
const removedTopics = memberProgress?.removed || [];
|
||||
const memberDone =
|
||||
memberProgress?.done.filter((id) => !removedTopics.includes(id)).length ||
|
||||
0;
|
||||
const memberLearning =
|
||||
memberProgress?.learning.filter((id) => !removedTopics.includes(id))
|
||||
.length || 0;
|
||||
const memberSkipped =
|
||||
memberProgress?.skipped.filter((id) => !removedTopics.includes(id))
|
||||
.length || 0;
|
||||
|
||||
const currProgress = member.progress.find((p) => p.resourceId === resourceId);
|
||||
const memberTotal = currProgress?.total || 0;
|
||||
|
||||
const progressPercentage = Math.round((memberDone / memberTotal) * 100);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isCurrentUser && (
|
||||
<div className="sticky top-1 z-50 mx-1 mb-0 mt-1 rounded-xl bg-gray-900 p-4 text-gray-300">
|
||||
<h2 className={'mb-1.5 text-base'}>
|
||||
Follow the Instructions below to update your progress
|
||||
</h2>
|
||||
<ul className="flex flex-col gap-1">
|
||||
<li className="leading-loose">
|
||||
<kbd className="rounded-md bg-yellow-200 px-2 py-1.5 text-xs text-gray-900">
|
||||
Right Mouse Click
|
||||
</kbd>{' '}
|
||||
on a topic to mark as{' '}
|
||||
<span className={'font-medium text-white'}>Done</span>.
|
||||
</li>
|
||||
<li className="leading-loose">
|
||||
<kbd className="rounded-md bg-yellow-200 px-2 py-1.5 text-xs text-gray-900">
|
||||
Shift
|
||||
</kbd>{' '}
|
||||
+{' '}
|
||||
<kbd className="rounded-md bg-yellow-200 px-2 py-1.5 text-xs text-gray-900">
|
||||
Click
|
||||
</kbd>{' '}
|
||||
on a topic to mark as{' '}
|
||||
<span className="font-medium text-white">In progress</span>.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-4">
|
||||
{!isCurrentUser && (
|
||||
<div className="mb-5 mt-0 text-left md:mt-4 md:text-center">
|
||||
<h2 className={'mb-1 text-lg font-bold md:text-2xl'}>
|
||||
{member.name}'s Progress
|
||||
</h2>
|
||||
<p
|
||||
className={
|
||||
'hidden text-xs text-gray-500 sm:text-sm md:block md:text-base'
|
||||
}
|
||||
>
|
||||
You are looking at {member.name}'s progress.{' '}
|
||||
<button
|
||||
className="text-blue-600 underline"
|
||||
onClick={onShowMyProgress}
|
||||
>
|
||||
View your progress
|
||||
</button>
|
||||
.
|
||||
</p>
|
||||
<p className={'block text-gray-500 md:hidden'}>
|
||||
<button
|
||||
className="text-blue-600 underline"
|
||||
onClick={onShowMyProgress}
|
||||
>
|
||||
View your progress.
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<p
|
||||
className={`-mx-4 mb-3 flex items-center justify-start border-b border-t px-4 py-2 text-sm sm:hidden ${
|
||||
isLoading ? 'striped-loader' : ''
|
||||
}`}
|
||||
>
|
||||
<span className="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900">
|
||||
<span>{progressPercentage}</span>% Done
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<span>{memberDone}</span> of <span>{memberTotal}</span> done
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
className={`-mx-4 mb-3 hidden items-center justify-center border-b border-t py-2 text-sm sm:flex ${
|
||||
isLoading ? 'striped-loader' : ''
|
||||
}`}
|
||||
>
|
||||
<span className="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900">
|
||||
<span>{progressPercentage}</span>% Done
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<span>{memberDone}</span> completed
|
||||
</span>
|
||||
<span className="mx-1.5 text-gray-400">·</span>
|
||||
<span>
|
||||
<span data-progress-learning="">{memberLearning}</span> in progress
|
||||
</span>
|
||||
|
||||
{memberSkipped > 0 && (
|
||||
<>
|
||||
<span className="mx-1.5 text-gray-400">·</span>
|
||||
<span>
|
||||
<span data-progress-skipped="">{memberSkipped}</span> skipped
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
<span className="mx-1.5 text-gray-400">·</span>
|
||||
<span>
|
||||
<span data-progress-total="">{memberTotal}</span> Total
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import { GroupRoadmapItem } from './GroupRoadmapItem';
|
||||
import { getUrlParams, setUrlParams } from '../../lib/browser';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import { MemberProgressModal } from './MemberProgressModal';
|
||||
import { MemberCustomProgressModal } from './MemberCustomProgressModal';
|
||||
|
||||
export type UserProgress = {
|
||||
resourceTitle: string;
|
||||
@@ -153,15 +152,10 @@ export function TeamProgressPage() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ProgressModal =
|
||||
showMemberProgress && !showMemberProgress.isCustomResource
|
||||
? MemberProgressModal
|
||||
: MemberCustomProgressModal;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showMemberProgress && (
|
||||
<ProgressModal
|
||||
<MemberProgressModal
|
||||
member={showMemberProgress.member}
|
||||
teamId={teamId}
|
||||
resourceId={showMemberProgress.resourceId}
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
import { RoadmapActionDropdown } from './RoadmapActionDropdown';
|
||||
import { UpdateTeamResourceModal } from '../CreateTeam/UpdateTeamResourceModal';
|
||||
import { ShareOptionsModal } from '../ShareOptions/ShareOptionsModal';
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
export function TeamRoadmaps() {
|
||||
const { t: teamId } = getUrlParams();
|
||||
@@ -429,12 +428,7 @@ export function TeamRoadmaps() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'grid grid-cols-1 p-2.5',
|
||||
canManageCurrentTeam
|
||||
? 'sm:grid-cols-[auto_172px]'
|
||||
: 'sm:grid-cols-[auto_110px]'
|
||||
)}
|
||||
className="grid grid-cols-1 p-2.5 sm:grid-cols-[auto_110px]"
|
||||
key={resourceConfig.resourceId}
|
||||
>
|
||||
<div className="mb-3 grid grid-cols-1 sm:mb-0">
|
||||
@@ -485,18 +479,6 @@ export function TeamRoadmaps() {
|
||||
<ExternalLink className="inline-block h-4 w-4" />
|
||||
Visit
|
||||
</a>
|
||||
{canManageCurrentTeam && (
|
||||
<a
|
||||
href={editorLink}
|
||||
className={
|
||||
'ml-2 flex items-center gap-2 rounded-md border border-gray-800 bg-gray-900 px-2.5 py-1.5 text-xs text-white hover:bg-gray-800 focus:outline-none'
|
||||
}
|
||||
target={'_blank'}
|
||||
>
|
||||
<PenSquare className="inline-block h-4 w-4" />
|
||||
Edit
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import CloseIcon from '../../icons/close.svg';
|
||||
import SpinnerIcon from '../../icons/spinner.svg';
|
||||
|
||||
@@ -8,13 +8,13 @@ import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { useToggleTopic } from '../../hooks/use-toggle-topic';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import type { ResourceType } from '../../lib/resource-progress';
|
||||
import {
|
||||
isTopicDone,
|
||||
refreshProgressCounters,
|
||||
renderTopicProgress,
|
||||
updateResourceProgress as updateResourceProgressApi,
|
||||
} from '../../lib/resource-progress';
|
||||
import type { ResourceType } from '../../lib/resource-progress';
|
||||
import { pageProgressMessage, sponsorHidden } from '../../stores/page';
|
||||
import { TopicProgressButton } from './TopicProgressButton';
|
||||
import { ContributionForm } from './ContributionForm';
|
||||
@@ -95,13 +95,13 @@ export function TopicDetail(props: TopicDetailProps) {
|
||||
resourceId,
|
||||
resourceType,
|
||||
},
|
||||
oldIsDone ? 'pending' : 'done',
|
||||
),
|
||||
oldIsDone ? 'pending' : 'done'
|
||||
)
|
||||
)
|
||||
.then(({ done = [] }) => {
|
||||
renderTopicProgress(
|
||||
topicId,
|
||||
done.includes(topicId) ? 'done' : 'pending',
|
||||
done.includes(topicId) ? 'done' : 'pending'
|
||||
);
|
||||
refreshProgressCounters();
|
||||
})
|
||||
@@ -149,7 +149,7 @@ export function TopicDetail(props: TopicDetailProps) {
|
||||
Accept: 'text/html',
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
)
|
||||
.then(({ response }) => {
|
||||
if (!response) {
|
||||
@@ -163,7 +163,7 @@ export function TopicDetail(props: TopicDetailProps) {
|
||||
// We only need the inner HTML of the #main-content
|
||||
const node = new DOMParser().parseFromString(
|
||||
response as string,
|
||||
'text/html',
|
||||
'text/html'
|
||||
);
|
||||
topicHtml = node?.getElementById('main-content')?.outerHTML || '';
|
||||
} else {
|
||||
@@ -171,7 +171,7 @@ export function TopicDetail(props: TopicDetailProps) {
|
||||
setTopicTitle((response as RoadmapContentDocument)?.title || '');
|
||||
topicHtml = markdownToHtml(
|
||||
(response as RoadmapContentDocument)?.description || '',
|
||||
false,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
@@ -184,10 +184,6 @@ export function TopicDetail(props: TopicDetailProps) {
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) topicRef?.current?.focus();
|
||||
}, [isActive]);
|
||||
|
||||
if (!isActive) {
|
||||
return null;
|
||||
}
|
||||
@@ -198,8 +194,7 @@ export function TopicDetail(props: TopicDetailProps) {
|
||||
<div className={'relative z-50'}>
|
||||
<div
|
||||
ref={topicRef}
|
||||
tabIndex={0}
|
||||
className="fixed right-0 top-0 z-40 h-screen w-full overflow-y-auto bg-white p-4 focus:outline-0 sm:max-w-[600px] sm:p-6"
|
||||
className="fixed right-0 top-0 z-40 h-screen w-full overflow-y-auto bg-white p-4 sm:max-w-[600px] sm:p-6"
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="flex w-full justify-center">
|
||||
@@ -284,7 +279,7 @@ export function TopicDetail(props: TopicDetailProps) {
|
||||
<span
|
||||
className={cn(
|
||||
'mr-2 inline-block rounded px-1.5 py-1 text-xs uppercase no-underline',
|
||||
linkTypes[link.type],
|
||||
linkTypes[link.type]
|
||||
)}
|
||||
>
|
||||
{link.type.charAt(0).toUpperCase() +
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { ErrorIcon } from "../ReactIcons/ErrorIcon";
|
||||
import { Spinner } from "../ReactIcons/Spinner";
|
||||
|
||||
type ProgressLoadingErrorProps = {
|
||||
isLoading: boolean;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export function ProgressLoadingError(props: ProgressLoadingErrorProps) {
|
||||
const { isLoading, error } = props;
|
||||
|
||||
return (
|
||||
<div className="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden 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">
|
||||
<div className="flex items-center">
|
||||
{isLoading && (
|
||||
<>
|
||||
<Spinner className="h-6 w-6" isDualRing={false} />
|
||||
<span className="ml-3 text-lg font-semibold">
|
||||
Loading user progress...
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<>
|
||||
<ErrorIcon additionalClasses="h-6 w-6 text-red-500" />
|
||||
<span className="ml-3 text-lg font-semibold">{error}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
import { useEffect, useMemo, useRef, useState, type RefObject } from 'react';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { useKeydown } from '../../hooks/use-keydown';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import type { ResourceType } from '../../lib/resource-progress';
|
||||
import { topicSelectorAll } from '../../lib/resource-progress';
|
||||
import CloseIcon from '../../icons/close.svg';
|
||||
import { deleteUrlParam, getUrlParams } from '../../lib/browser';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import type { GetRoadmapResponse } from '../CustomRoadmap/CustomRoadmap';
|
||||
import { ReadonlyEditor } from '../../../editor/readonly-editor';
|
||||
import { ProgressLoadingError } from './ProgressLoadingError';
|
||||
import { UserProgressModalHeader } from './UserProgressModalHeader';
|
||||
|
||||
export type ProgressMapProps = {
|
||||
userId?: string;
|
||||
resourceId: string;
|
||||
resourceType: ResourceType;
|
||||
onClose?: () => void;
|
||||
isCustomResource?: boolean;
|
||||
};
|
||||
|
||||
type UserProgressResponse = {
|
||||
user: {
|
||||
_id: string;
|
||||
name: string;
|
||||
};
|
||||
progress: {
|
||||
total: number;
|
||||
done: string[];
|
||||
learning: string[];
|
||||
skipped: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export function UserCustomProgressModal(props: ProgressMapProps) {
|
||||
const {
|
||||
resourceId,
|
||||
resourceType,
|
||||
userId: propUserId,
|
||||
onClose: onModalClose,
|
||||
isCustomResource,
|
||||
} = props;
|
||||
|
||||
const { s: userId = propUserId } = getUrlParams();
|
||||
if (!userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resourceSvgEl = useRef<HTMLDivElement>(null);
|
||||
const popupBodyEl = useRef<HTMLDivElement>(null);
|
||||
const currentUser = useAuth();
|
||||
|
||||
const [roadmap, setRoadmap] = useState<GetRoadmapResponse | null>(null);
|
||||
const [showModal, setShowModal] = useState(!!userId);
|
||||
const [progressResponse, setProgressResponse] =
|
||||
useState<UserProgressResponse>();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
async function getUserProgress(
|
||||
userId: string,
|
||||
resourceType: string,
|
||||
resourceId: string,
|
||||
): Promise<UserProgressResponse | undefined> {
|
||||
const { error, response } = await httpGet<UserProgressResponse>(
|
||||
`${
|
||||
import.meta.env.PUBLIC_API_URL
|
||||
}/v1-get-user-progress/${userId}?resourceType=${resourceType}&resourceId=${resourceId}`,
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
throw error || new Error('Something went wrong. Please try again!');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function getRoadmapSVG(): Promise<GetRoadmapResponse> {
|
||||
const { error, response: roadmapData } = await httpGet<GetRoadmapResponse>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-roadmap/${resourceId}`,
|
||||
);
|
||||
if (error || !roadmapData) {
|
||||
throw error || new Error('Something went wrong. Please try again!');
|
||||
}
|
||||
|
||||
setRoadmap(roadmapData);
|
||||
return roadmapData;
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
deleteUrlParam('s');
|
||||
setError('');
|
||||
setShowModal(false);
|
||||
|
||||
if (onModalClose) {
|
||||
onModalClose();
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
useKeydown('Escape', () => {
|
||||
onClose();
|
||||
});
|
||||
|
||||
useOutsideClick(popupBodyEl, () => {
|
||||
onClose();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!resourceId || !resourceType || !userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
Promise.all([
|
||||
getRoadmapSVG(),
|
||||
getUserProgress(userId, resourceType, resourceId),
|
||||
])
|
||||
.then(([_, user]) => {
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
setProgressResponse(user);
|
||||
})
|
||||
.catch((err) => {
|
||||
setError(err?.message || 'Something went wrong. Please try again!');
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [userId]);
|
||||
|
||||
if (currentUser?.id === userId) {
|
||||
deleteUrlParam('s');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!showModal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isLoading || error) {
|
||||
return <ProgressLoadingError isLoading={isLoading} error={error || ''} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id={'user-progress-modal'}
|
||||
className="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50"
|
||||
>
|
||||
<div className="relative mx-auto h-full w-full max-w-4xl p-4 md:h-auto">
|
||||
<div
|
||||
ref={popupBodyEl}
|
||||
className={`popup-body relative rounded-lg bg-white pt-[1px] shadow`}
|
||||
>
|
||||
<UserProgressModalHeader
|
||||
isLoading={isLoading}
|
||||
progressResponse={progressResponse}
|
||||
/>
|
||||
|
||||
<div ref={resourceSvgEl} className="px-4 pb-2">
|
||||
<ReadonlyEditor
|
||||
variant="modal"
|
||||
roadmap={roadmap!}
|
||||
className="min-h-[400px]"
|
||||
onRendered={(wrapperRef: RefObject<HTMLDivElement>) => {
|
||||
const {
|
||||
done = [],
|
||||
learning = [],
|
||||
skipped = [],
|
||||
} = progressResponse?.progress || {};
|
||||
|
||||
done?.forEach((topicId: string) => {
|
||||
topicSelectorAll(topicId, wrapperRef?.current!).forEach(
|
||||
(el) => {
|
||||
el.classList.add('done');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
learning?.forEach((topicId: string) => {
|
||||
topicSelectorAll(topicId, wrapperRef?.current!).forEach(
|
||||
(el) => {
|
||||
el.classList.add('learning');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
skipped?.forEach((topicId: string) => {
|
||||
topicSelectorAll(topicId, wrapperRef?.current!).forEach(
|
||||
(el) => {
|
||||
el.classList.add('skipped');
|
||||
},
|
||||
);
|
||||
});
|
||||
}}
|
||||
fontFamily="Balsamiq Sans"
|
||||
fontURL="/fonts/balsamiq.woff2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`absolute right-2.5 top-3 ml-auto inline-flex items-center rounded-lg bg-gray-100 bg-transparent p-1.5 text-sm text-gray-400 hover:text-gray-900 lg:hidden`}
|
||||
onClick={onClose}
|
||||
>
|
||||
<img alt={'close'} src={CloseIcon.src} className="h-4 w-4" />
|
||||
<span className="sr-only">Close modal</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,8 +9,9 @@ import { topicSelectorAll } from '../../lib/resource-progress';
|
||||
import CloseIcon from '../../icons/close.svg';
|
||||
import { deleteUrlParam, getUrlParams } from '../../lib/browser';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import { ProgressLoadingError } from './ProgressLoadingError';
|
||||
import { UserProgressModalHeader } from './UserProgressModalHeader';
|
||||
import { Spinner } from '../ReactIcons/Spinner';
|
||||
import { ErrorIcon } from '../ReactIcons/ErrorIcon';
|
||||
import { renderFlowJSON } from '../../../renderer/renderer';
|
||||
|
||||
export type ProgressMapProps = {
|
||||
userId?: string;
|
||||
@@ -20,7 +21,7 @@ export type ProgressMapProps = {
|
||||
isCustomResource?: boolean;
|
||||
};
|
||||
|
||||
export type UserProgressResponse = {
|
||||
type UserProgressResponse = {
|
||||
user: {
|
||||
_id: string;
|
||||
name: string;
|
||||
@@ -39,6 +40,7 @@ export function UserProgressModal(props: ProgressMapProps) {
|
||||
resourceType,
|
||||
userId: propUserId,
|
||||
onClose: onModalClose,
|
||||
isCustomResource,
|
||||
} = props;
|
||||
|
||||
const { s: userId = propUserId } = getUrlParams();
|
||||
@@ -67,6 +69,12 @@ export function UserProgressModal(props: ProgressMapProps) {
|
||||
resourceJsonUrl += `/best-practices/${resourceId}.json`;
|
||||
}
|
||||
|
||||
if (isCustomResource) {
|
||||
resourceJsonUrl = `${
|
||||
import.meta.env.PUBLIC_API_URL
|
||||
}/v1-get-roadmap/${resourceId}`;
|
||||
}
|
||||
|
||||
async function getUserProgress(
|
||||
userId: string,
|
||||
resourceType: string,
|
||||
@@ -93,6 +101,12 @@ export function UserProgressModal(props: ProgressMapProps) {
|
||||
throw error || new Error('Something went wrong. Please try again!');
|
||||
}
|
||||
|
||||
if (isCustomResource) {
|
||||
return await renderFlowJSON({
|
||||
nodes: roadmapJson?.nodes || [],
|
||||
edges: roadmapJson?.edges || [],
|
||||
});
|
||||
}
|
||||
return await wireframeJSONToSVG(roadmapJson, {
|
||||
fontURL: '/fonts/balsamiq.woff2',
|
||||
});
|
||||
@@ -166,6 +180,14 @@ export function UserProgressModal(props: ProgressMapProps) {
|
||||
el.removeAttribute('data-group-id');
|
||||
});
|
||||
|
||||
svg.querySelectorAll('[data-node-id]').forEach((el) => {
|
||||
el.removeAttribute('data-node-id');
|
||||
});
|
||||
|
||||
svg.querySelectorAll('[data-type]').forEach((el) => {
|
||||
el.removeAttribute('data-type');
|
||||
});
|
||||
|
||||
setResourceSvg(svg);
|
||||
setProgressResponse(user);
|
||||
})
|
||||
@@ -177,6 +199,16 @@ export function UserProgressModal(props: ProgressMapProps) {
|
||||
});
|
||||
}, [userId]);
|
||||
|
||||
const user = progressResponse?.user;
|
||||
const progress = progressResponse?.progress;
|
||||
|
||||
const userProgressTotal = progress?.total || 0;
|
||||
const userDone = progress?.done?.length || 0;
|
||||
const progressPercentage =
|
||||
Math.round((userDone / userProgressTotal) * 100) || 0;
|
||||
const userLearning = progress?.learning?.length || 0;
|
||||
const userSkipped = progress?.skipped?.length || 0;
|
||||
|
||||
if (currentUser?.id === userId) {
|
||||
deleteUrlParam('s');
|
||||
return null;
|
||||
@@ -187,7 +219,31 @@ export function UserProgressModal(props: ProgressMapProps) {
|
||||
}
|
||||
|
||||
if (isLoading || error) {
|
||||
return <ProgressLoadingError isLoading={isLoading} error={error} />;
|
||||
return (
|
||||
<div className="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden 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">
|
||||
<div className="flex items-center">
|
||||
{isLoading && (
|
||||
<>
|
||||
<Spinner className="h-6 w-6" isDualRing={false} />
|
||||
<span className="ml-3 text-lg font-semibold">
|
||||
Loading user progress...
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<>
|
||||
<ErrorIcon additionalClasses="h-6 w-6 text-red-500" />
|
||||
<span className="ml-3 text-lg font-semibold">{error}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -200,10 +256,62 @@ export function UserProgressModal(props: ProgressMapProps) {
|
||||
ref={popupBodyEl}
|
||||
className={`popup-body relative rounded-lg bg-white pt-[1px] shadow`}
|
||||
>
|
||||
<UserProgressModalHeader
|
||||
isLoading={isLoading}
|
||||
progressResponse={progressResponse}
|
||||
/>
|
||||
<div className="p-4">
|
||||
<div className="mb-5 mt-0 min-h-[28px] text-left sm:text-center md:mt-4 md:h-[60px]">
|
||||
<h2 className={'mb-1 text-lg font-bold md:text-2xl'}>
|
||||
{user?.name}'s Progress
|
||||
</h2>
|
||||
<p
|
||||
className={
|
||||
'hidden text-xs text-gray-500 sm:text-sm md:block md:text-base'
|
||||
}
|
||||
>
|
||||
You can close this popup and start tracking your progress.
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
className={`-mx-4 mb-3 flex items-center justify-start border-b border-t px-4 py-2 text-sm sm:hidden`}
|
||||
>
|
||||
<span className="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900">
|
||||
<span>{progressPercentage}</span>% Done
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<span>{userDone}</span> of <span>{userProgressTotal}</span> done
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
className={`-mx-4 mb-3 hidden items-center justify-center border-b border-t py-2 text-sm sm:flex ${
|
||||
isLoading ? 'striped-loader' : ''
|
||||
}`}
|
||||
>
|
||||
<span className="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900">
|
||||
<span>{progressPercentage}</span>% Done
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<span>{userDone}</span> completed
|
||||
</span>
|
||||
<span className="mx-1.5 text-gray-400">·</span>
|
||||
<span>
|
||||
<span>{userLearning}</span> in progress
|
||||
</span>
|
||||
|
||||
{userSkipped > 0 && (
|
||||
<>
|
||||
<span className="mx-1.5 text-gray-400">·</span>
|
||||
<span>
|
||||
<span>{userSkipped}</span> skipped
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
<span className="mx-1.5 text-gray-400">·</span>
|
||||
<span>
|
||||
<span>{userProgressTotal}</span> Total
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={resourceSvgEl}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import type { UserProgressResponse } from './UserProgressModal';
|
||||
|
||||
type UserProgressModalHeaderProps = {
|
||||
isLoading: boolean;
|
||||
progressResponse: UserProgressResponse | undefined;
|
||||
};
|
||||
|
||||
export function UserProgressModalHeader(props: UserProgressModalHeaderProps) {
|
||||
const { isLoading, progressResponse } = props;
|
||||
|
||||
const user = progressResponse?.user;
|
||||
const progress = progressResponse?.progress;
|
||||
|
||||
const userProgressTotal = progress?.total || 0;
|
||||
const userDone = progress?.done?.length || 0;
|
||||
const progressPercentage =
|
||||
Math.round((userDone / userProgressTotal) * 100) || 0;
|
||||
const userLearning = progress?.learning?.length || 0;
|
||||
const userSkipped = progress?.skipped?.length || 0;
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="mb-5 mt-0 min-h-[28px] text-left sm:text-center md:mt-4 md:h-[60px]">
|
||||
<h2 className={'mb-1 text-lg font-bold md:text-2xl'}>
|
||||
{user?.name}'s Progress
|
||||
</h2>
|
||||
<p
|
||||
className={
|
||||
'hidden text-xs text-gray-500 sm:text-sm md:block md:text-base'
|
||||
}
|
||||
>
|
||||
You can close this popup and start tracking your progress.
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
className={`-mx-4 mb-3 flex items-center justify-start border-b border-t px-4 py-2 text-sm sm:hidden`}
|
||||
>
|
||||
<span className="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900">
|
||||
<span>{progressPercentage}</span>% Done
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<span>{userDone}</span> of <span>{userProgressTotal}</span> done
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
className={`-mx-4 mb-3 hidden items-center justify-center border-b border-t py-2 text-sm sm:flex ${
|
||||
isLoading ? 'striped-loader' : ''
|
||||
}`}
|
||||
>
|
||||
<span className="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900">
|
||||
<span>{progressPercentage}</span>% Done
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<span>{userDone}</span> completed
|
||||
</span>
|
||||
<span className="mx-1.5 text-gray-400">·</span>
|
||||
<span>
|
||||
<span>{userLearning}</span> in progress
|
||||
</span>
|
||||
|
||||
{userSkipped > 0 && (
|
||||
<>
|
||||
<span className="mx-1.5 text-gray-400">·</span>
|
||||
<span>
|
||||
<span>{userSkipped}</span> skipped
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
<span className="mx-1.5 text-gray-400">·</span>
|
||||
<span>
|
||||
<span>{userProgressTotal}</span> Total
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -67,7 +67,7 @@ Unlike browser cache which serves a single user, proxy caches may serve hundreds
|
||||
|
||||
A Reverse proxy cache or surrogate cache is implemented close to the origin servers in order to reduce the load on the server. Unlike proxy caches which are implemented by ISPs etc to reduce the bandwidth usage in a network, surrogates or reverse proxy caches are implemented near the origin servers by the server administrators to reduce the load on the server.
|
||||
|
||||

|
||||

|
||||
|
||||
Although you can control the reverse proxy caches (since it is implemented by you on your server) you can not avoid or control browser and proxy caches. And if your website is not configured to use these caches properly, it will still be cached using whatever defaults are set on these caches.
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
Let's see how we can use the `alert`, `prompt` and `confirm` functions to interact with the user.
|
||||
|
||||
## alert()
|
||||
|
||||
The `alert()` method displays an alert box with a specified message and an OK button.
|
||||
|
||||
```js
|
||||
alert('Hello World!');
|
||||
```
|
||||
|
||||
## prompt()
|
||||
|
||||
The `prompt()` method displays a dialog box that prompts the visitor for input. A prompt box is often used if you want the user to input a value before entering a page. The `prompt()` method returns the input value if the user clicks OK. If the user clicks Cancel, the method returns `null`.
|
||||
|
||||
```js
|
||||
const name = prompt('What is your name?');
|
||||
console.log(name);
|
||||
```
|
||||
|
||||
## confirm()
|
||||
|
||||
The `confirm()` method displays a dialog box with a specified message, along with an OK and a Cancel button. This is often used to confirm or verify something from the user.
|
||||
|
||||
```js
|
||||
const result = confirm('Are you sure?');
|
||||
console.log(result); // true/false
|
||||
```
|
||||
@@ -1,32 +0,0 @@
|
||||
You can add a new element to the DOM using the `appendChild` or `insertBefore` method.
|
||||
|
||||
## appendChild
|
||||
|
||||
The `appendChild` method adds a new element as the last child of the specified parent element.
|
||||
|
||||
```js
|
||||
const roadmapWrapper = document.querySelector('.roadmap-wrapper');
|
||||
|
||||
const roadmap = document.createElement('div');
|
||||
roadmap.id = 'javascript-roadmap';
|
||||
|
||||
roadmapWrapper.appendChild(roadmapTitle);
|
||||
```
|
||||
|
||||
In the example above, the `roadmap` element is added as the last child of the `roadmapWrapper` element.
|
||||
|
||||
## insertBefore
|
||||
|
||||
The `insertBefore` method adds a new element before the specified child element.
|
||||
|
||||
```js
|
||||
const roadmapWrapper = document.querySelector('.roadmap-wrapper');
|
||||
|
||||
const roadmap = document.createElement('div');
|
||||
roadmap.id = 'javascript-roadmap';
|
||||
|
||||
const roadmapTitle = document.querySelector('#roadmap-title');
|
||||
roadmapWrapper.insertBefore(roadmap, roadmapTitle);
|
||||
```
|
||||
|
||||
In the example above, the `roadmap` element is added before the `roadmapTitle` element.
|
||||
@@ -1,27 +0,0 @@
|
||||
The difference between Asynchronous and Synchronous code is that Asynchronous code does not block the execution of the program while Synchronous code does.
|
||||
|
||||
## Asynchronous code
|
||||
|
||||
Asynchronous code is executed in the background and it does not block the execution of the program. It is usually used to perform tasks that take a long time to complete, such as network requests.
|
||||
|
||||
```js
|
||||
console.log('Before');
|
||||
|
||||
setTimeout(() => {
|
||||
console.log('Hello');
|
||||
}, 1000);
|
||||
|
||||
console.log('After');
|
||||
```
|
||||
|
||||
## Synchronous code
|
||||
|
||||
Synchronous code is executed in sequence and it blocks the execution of the program until it is completed. If a task takes a long time to complete, everything else waits.
|
||||
|
||||
```js
|
||||
console.log('Before');
|
||||
|
||||
for (let i = 0; i < 1000000000; i++) {}
|
||||
|
||||
console.log('After');
|
||||
```
|
||||
@@ -1,28 +0,0 @@
|
||||
You can use `break` and `continue` in loops to alter the flow of the loop. `break` will stop the loop from continuing, and `continue` will skip the current iteration and continue the loop.
|
||||
|
||||
```js
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (i === 1) {
|
||||
continue; // skips the rest of the code in the loop
|
||||
}
|
||||
console.log(`i: ${i}`);
|
||||
}
|
||||
|
||||
// Output:
|
||||
// i: 0
|
||||
// i: 2
|
||||
// i: 3
|
||||
// i: 4
|
||||
```
|
||||
|
||||
```js
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (i === 1) {
|
||||
break; // stops the loop
|
||||
}
|
||||
console.log(`i: ${i}`);
|
||||
}
|
||||
|
||||
// Output:
|
||||
// i: 0
|
||||
```
|
||||
@@ -1,48 +0,0 @@
|
||||
**Callback hell**, often referred to as **Pyramid of Doom**, describes a situation in JavaScript where multiple nested callbacks become difficult to manage, leading to unreadable and unmaintainable code. It often arises when performing multiple asynchronous operations that depend on the completion of previous operations. The code starts to take on a pyramidal shape due to the nesting.
|
||||
|
||||
## Example of callback hell
|
||||
|
||||
```js
|
||||
callAsync1(function () {
|
||||
callAsync2(function () {
|
||||
callAsync3(function () {
|
||||
callAsync4(function () {
|
||||
callAsync5(function () {
|
||||
// ...
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Strategies to avoid callback hell
|
||||
|
||||
Developers can address or avoid callback hell by using strategies like modularizing the code into named functions, using asynchronous control flow libraries, or leveraging modern JavaScript features like Promises and `async/await` to write more linear, readable asynchronous code.
|
||||
|
||||
### Promise chaining
|
||||
|
||||
```js
|
||||
callAsync1()
|
||||
.then(() => callAsync2())
|
||||
.then(() => callAsync3())
|
||||
.then(() => callAsync4())
|
||||
.then(() => callAsync5())
|
||||
.catch((err) => console.error(err));
|
||||
```
|
||||
|
||||
### Async/await
|
||||
|
||||
```js
|
||||
async function asyncCall() {
|
||||
try {
|
||||
await callAsync1();
|
||||
await callAsync2();
|
||||
await callAsync3();
|
||||
await callAsync4();
|
||||
await callAsync5();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,18 +0,0 @@
|
||||
A closure is a function that has access to its outer function scope even after the outer function has returned. This means a closure can remember and access variables and arguments of its outer function even after the function has finished.
|
||||
|
||||
```js
|
||||
function outer() {
|
||||
const name = 'Roadmap';
|
||||
|
||||
function inner() {
|
||||
console.log(name);
|
||||
}
|
||||
|
||||
return inner;
|
||||
}
|
||||
|
||||
const closure = outer();
|
||||
closure(); // Roadmap
|
||||
```
|
||||
|
||||
In the above example, the `inner` function has access to the `name` variable of the `outer` function even after the `outer` function has returned. Therefore, the `inner` function forms a closure.
|
||||
@@ -1,8 +0,0 @@
|
||||
The Comma Operator `,` evaluates each of its operands (from left to right) and returns the value of the last operand.
|
||||
|
||||
```js
|
||||
let x = 1;
|
||||
x = (x++, x);
|
||||
|
||||
console.log(x); // 2
|
||||
```
|
||||
@@ -1,9 +0,0 @@
|
||||
To create a new DOM element, you can use the `document.createElement` method. It accepts a tag name as an argument and returns a new element with the specified tag name. You can set attributes to the element.
|
||||
|
||||
```js
|
||||
const div = document.createElement('div');
|
||||
|
||||
div.id = 'roadmap-wrapper';
|
||||
div.setAttribute('data-id', 'javascript');
|
||||
console.log(div); // <div id="roadmap-wrapper" data-id="javascript"></div>
|
||||
```
|
||||
@@ -1,33 +0,0 @@
|
||||
You can use the `CustomEvent` constructor to create a custom event. The `CustomEvent` constructor accepts two arguments: the event name and an optional object that specifies the event options. And you can use the `dispatchEvent` method to dispatch the custom event on the target element/document.
|
||||
|
||||
## Creating Custom Events
|
||||
|
||||
```js
|
||||
const event = new CustomEvent('roadmap-updated', {
|
||||
detail: { name: 'JavaScript' },
|
||||
});
|
||||
element.dispatchEvent(event);
|
||||
```
|
||||
|
||||
## Listening for Custom Events
|
||||
|
||||
You can listen for custom events using the `addEventListener` method. The `addEventListener` method accepts the event name and a callback function that is called when the event is dispatched.
|
||||
|
||||
```js
|
||||
element.addEventListener('roadmap-updated', (event) => {
|
||||
console.log(event.detail); // { name: 'JavaScript' }
|
||||
});
|
||||
```
|
||||
|
||||
## Removing Event Listeners
|
||||
|
||||
You can remove event listeners using the `removeEventListener` method. The `removeEventListener` method accepts the event name and the callback function that was used to add the event listener.
|
||||
|
||||
```js
|
||||
function handleEvent(event) {
|
||||
console.log(event.detail); // { name: 'JavaScript' }
|
||||
}
|
||||
|
||||
element.addEventListener('roadmap-updated', handleEvent);
|
||||
element.removeEventListener('roadmap-updated', handleEvent);
|
||||
```
|
||||
@@ -1,38 +0,0 @@
|
||||
Debugging JavaScript code can be achieved through various methods and tools. Here's a basic guide:
|
||||
|
||||
## Console Logging:
|
||||
|
||||
You can use `console.log()`, `console.warn()`, `console.error()`, etc., to print values, variables, or messages to the browser's developer console.
|
||||
|
||||
```js
|
||||
console.log('Value of x:', x);
|
||||
```
|
||||
|
||||
## Browser Developer Tools:
|
||||
|
||||
Most modern browsers come equipped with developer tools. You can access these tools by pressing `F12` or right-clicking on the web page and selecting `Inspect` or `Inspect Element`.
|
||||
|
||||
- **Sources Tab**: Allows you to see the loaded scripts, set breakpoints, and step through the code.
|
||||
- **Console Tab**: Displays console outputs and allows for interactive JavaScript execution.
|
||||
- **Network Tab**: Helps in checking network requests and responses.
|
||||
|
||||
## Setting Breakpoints:
|
||||
|
||||
In the `Sources` tab of the browser's developer tools, you can click on a line number to set a breakpoint. The code execution will pause at this line, allowing you to inspect variables, the call stack, and continue step-by-step.
|
||||
|
||||
## Debugger Statement:
|
||||
|
||||
Inserting the `debugger;` statement in your code will act as a breakpoint when the browser developer tools are open. Execution will pause at the `debugger;` line.
|
||||
|
||||
```js
|
||||
function myFunction() {
|
||||
debugger; // Execution will pause here when dev tools are open
|
||||
// ... rest of the code
|
||||
}
|
||||
```
|
||||
|
||||
## Call Stack and Scope:
|
||||
|
||||
In the developer tools, when paused on a breakpoint or `debugger;` statement, you can inspect the `call stack` to see the sequence of function calls. The `Scope` panel will show you the values of local and global variables.
|
||||
|
||||
Remember, debugging is an iterative process. It often involves setting breakpoints, checking variables, adjusting code, and re-running to ensure correctness.
|
||||
@@ -1,25 +0,0 @@
|
||||
The main difference between `defer` and `async` is the order of execution.
|
||||
|
||||
## Defer attribute
|
||||
|
||||
A `<script>` element with a `defer` attribute, it will continue to load the HTML page and render it while the script is being downloaded. The script is executed after the HTML page has been completely parsed. `defer` scripts maintain their order in the document.
|
||||
|
||||
```html
|
||||
<script defer src="script1.js"></script>
|
||||
<script defer src="script2.js"></script>
|
||||
```
|
||||
|
||||
In the example above, `script1.js` will be executed before `script2.js`. The browser will download both scripts in parallel, but `script1.js` will be executed after the HTML page has been parsed and `script2.js` will be executed after `script1.js` has been executed.
|
||||
|
||||
## Async attribute
|
||||
|
||||
On the other hand, A `<script>` element with an `async` attribute, it will pause the HTML parser and execute the script immediately after it has been downloaded. The HTML parsing will resume after the script has been executed.
|
||||
|
||||
```html
|
||||
<script async src="script1.js"></script>
|
||||
<script async src="script2.js"></script>
|
||||
```
|
||||
|
||||
In the example above, the browser will download both scripts in parallel, and execute them as soon as they are downloaded. The order of execution is not guaranteed.
|
||||
|
||||
To know more you can check [this diagram](https://roadmap.sh/guides/avoid-render-blocking-javascript-with-async-defer) from us that explains the difference between `defer` and `async` in a visual way.
|
||||
@@ -1,14 +0,0 @@
|
||||
The `do...while` statement creates a loop that executes a block of code once, before checking if the condition is `true`, then it will repeat the loop as long as the condition is `true`.
|
||||
|
||||
```js
|
||||
let i = 0;
|
||||
|
||||
do {
|
||||
console.log(i);
|
||||
i++;
|
||||
} while (i < 3);
|
||||
|
||||
// 0
|
||||
// 1
|
||||
// 2
|
||||
```
|
||||
@@ -1,7 +0,0 @@
|
||||
The `==` equality operator converts the operands if they are not of the same type, then applies strict comparison. The `===` strict equality operator only considers values equal that have the same type.
|
||||
|
||||
```js
|
||||
console.log(1 == '1'); // true
|
||||
console.log(1 === '1'); // false
|
||||
console.log(1 === 1); // true
|
||||
```
|
||||
@@ -1,24 +0,0 @@
|
||||
In order to handle errors in async/await, we can use the `try/catch` statement.
|
||||
|
||||
## Rejecting a promise
|
||||
|
||||
```js
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
reject(new Error('Something went wrong'));
|
||||
});
|
||||
```
|
||||
|
||||
## Try/catch statement
|
||||
|
||||
```js
|
||||
async function main() {
|
||||
try {
|
||||
const result = await promise;
|
||||
console.log(result);
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `catch` block will be executed when the promise is `rejected` or when an error is thrown inside the `try` block.
|
||||
@@ -1,38 +0,0 @@
|
||||
In order to handle errors in promises, we can use the `catch` method or the second argument of the `then` method.
|
||||
|
||||
## Rejecting a promise
|
||||
|
||||
```js
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
reject(new Error('Something went wrong'));
|
||||
});
|
||||
```
|
||||
|
||||
## Catch method
|
||||
|
||||
In this method, we can pass a `callback` function that will be called when the promise is `rejected`.
|
||||
|
||||
```js
|
||||
promise
|
||||
.then((result) => {
|
||||
console.log(result);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error.message);
|
||||
});
|
||||
```
|
||||
|
||||
## Second argument of the then method
|
||||
|
||||
In this method, we can pass two `callback` functions as arguments. The first one will be called when the promise is `resolved` and the second one will be called when the promise is `rejected`.
|
||||
|
||||
```js
|
||||
promise.then(
|
||||
(result) => {
|
||||
console.log(result);
|
||||
},
|
||||
(error) => {
|
||||
console.log(error.message);
|
||||
}
|
||||
);
|
||||
```
|
||||
@@ -1,19 +0,0 @@
|
||||
Event bubbling is a concept in the Document Object Model (DOM) that describes the way in which events propagate or "bubble up" through the hierarchy of nested elements in the DOM.
|
||||
|
||||
When an event, such as a mouse click, occurs on a DOM element, the event will be handled by the element first, then its parent element, and so on, until the event reaches the root element. This behavior is called event bubbling.
|
||||
|
||||
```js
|
||||
const parent = document.querySelector('.parent');
|
||||
const child = document.querySelector('.child');
|
||||
|
||||
// Scenario of clicking on the child element
|
||||
parent.addEventListener('click', () => {
|
||||
console.log('Handled Last');
|
||||
});
|
||||
|
||||
child.addEventListener('click', () => {
|
||||
console.log('Handled First');
|
||||
});
|
||||
```
|
||||
|
||||
In the above example, when you click on the `child` element, the event will be handled by the `child` element first, then its parent element, and so on, to the root element unless you stop the propagation (`event.stopPropagation()`) of the event.
|
||||
@@ -1,26 +0,0 @@
|
||||
The Event loop has two main components: the Call stack and the Callback queue.
|
||||
|
||||
## Call Stack
|
||||
|
||||
The Call stack is a data structure that stores the tasks that need to be executed. It is a LIFO (Last In, First Out) data structure, which means that the last task that was added to the Call stack will be the first one to be executed.
|
||||
|
||||
## Callback Queue
|
||||
|
||||
The Callback queue is a data structure that stores the tasks that have been completed and are ready to be executed. It is a FIFO (First In, First Out) data structure, which means that the first task that was added to the Callback queue will be the first one to be executed.
|
||||
|
||||
## Event Loop's Workflow:
|
||||
|
||||
1. Executes tasks from the Call Stack.
|
||||
2. For an asynchronous task, such as a timer, it runs in the background. JavaScript proceeds to the next task without waiting.
|
||||
3. When the asynchronous task concludes, its callback function is added to the Callback Queue.
|
||||
4. If the Call Stack is empty and there are tasks in the Callback Queue, the Event Loop transfers the first task from the Queue to the Call Stack for execution.
|
||||
|
||||
```js
|
||||
setTimeout(() => console.log('Hello from the timer'), 0);
|
||||
console.log('Hello from the main code');
|
||||
```
|
||||
|
||||
1. `setTimeout` is processed, and because it's asynchronous, its callback is placed in the Callback Queue.
|
||||
2. The next line, `console.log("Hello from the main code")`, is logged immediately.
|
||||
3. Although the timer duration is 0 milliseconds, its callback has to wait until the Call Stack is empty. After the main code logs, the callback is moved from the Callback Queue to the Call Stack and executed.
|
||||
4. The result is "Hello from the main code" being logged before "Hello from the timer".
|
||||
@@ -1,19 +0,0 @@
|
||||
Explicit binding is a way to explicitly state what the `this` keyword is going to be bound to using `call`, `apply` or `bind` methods of a function.
|
||||
|
||||
```js
|
||||
const roadmap = {
|
||||
name: 'JavaScript',
|
||||
};
|
||||
|
||||
function printName() {
|
||||
console.log(this.name);
|
||||
}
|
||||
|
||||
printName.call(roadmap); // JavaScript
|
||||
printName.apply(roadmap); // JavaScript
|
||||
|
||||
const printRoadmapName = printName.bind(roadmap);
|
||||
printRoadmapName(); // JavaScript
|
||||
```
|
||||
|
||||
In the above example, the `this` keyword inside the `printName()` function is explicitly bound to the `roadmap` object using `call`, `apply` or `bind` methods.
|
||||
@@ -1,12 +0,0 @@
|
||||
You can use the `filter()` method to filter an array based on a condition. The `filter()` method creates a new array with all elements that pass the test implemented by the provided function.
|
||||
|
||||
```js
|
||||
const numbers = [1, 2, 3, 4, 5, 6];
|
||||
|
||||
const evenNumbers = numbers.filter((number) => {
|
||||
return number % 2 === 0;
|
||||
});
|
||||
|
||||
console.log(numbers); // [1, 2, 3, 4, 5, 6]
|
||||
console.log(evenNumbers); // [2, 4, 6]
|
||||
```
|
||||
@@ -1,14 +0,0 @@
|
||||
The `finally` block will be executed when the promise is `resolved` or `rejected`.
|
||||
|
||||
```js
|
||||
promise
|
||||
.then((result) => {
|
||||
console.log(result);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
console.log('Finally Promise has settled');
|
||||
});
|
||||
```
|
||||
@@ -1,55 +0,0 @@
|
||||
There are serveral ways to find unique values in an array. Here are some of them:
|
||||
|
||||
## Using `Set`
|
||||
|
||||
```js
|
||||
const roadmaps = ['JavaScript', 'React', 'Node.js', 'Node.js', 'JavaScript'];
|
||||
const uniqueRoadmaps = [...new Set(roadmaps)];
|
||||
console.log(uniqueRoadmaps); // ['JavaScript', 'React', 'Node.js']
|
||||
```
|
||||
|
||||
## Using `filter()`
|
||||
|
||||
```js
|
||||
const roadmaps = ['JavaScript', 'React', 'Node.js', 'Node.js', 'JavaScript'];
|
||||
const uniqueRoadmaps = roadmaps.filter(
|
||||
(roadmap, index) => roadmaps.indexOf(roadmap) === index
|
||||
);
|
||||
console.log(uniqueRoadmaps); // ['JavaScript', 'React', 'Node.js']
|
||||
```
|
||||
|
||||
## Using `reduce()`
|
||||
|
||||
```js
|
||||
const roadmaps = ['JavaScript', 'React', 'Node.js', 'Node.js', 'JavaScript'];
|
||||
const uniqueRoadmaps = roadmaps.reduce((unique, roadmap) => {
|
||||
return unique.includes(roadmap) ? unique : [...unique, roadmap];
|
||||
}, []);
|
||||
console.log(uniqueRoadmaps); // ['JavaScript', 'React', 'Node.js']
|
||||
```
|
||||
|
||||
## Using `forEach()`
|
||||
|
||||
```js
|
||||
const roadmaps = ['JavaScript', 'React', 'Node.js', 'Node.js', 'JavaScript'];
|
||||
const uniqueRoadmaps = [];
|
||||
roadmaps.forEach((roadmap) => {
|
||||
if (!uniqueRoadmaps.includes(roadmap)) {
|
||||
uniqueRoadmaps.push(roadmap);
|
||||
}
|
||||
});
|
||||
console.log(uniqueRoadmaps); // ['JavaScript', 'React', 'Node.js']
|
||||
```
|
||||
|
||||
## Using `for...of`
|
||||
|
||||
```js
|
||||
const roadmaps = ['JavaScript', 'React', 'Node.js', 'Node.js', 'JavaScript'];
|
||||
const uniqueRoadmaps = [];
|
||||
for (const roadmap of roadmaps) {
|
||||
if (!uniqueRoadmaps.includes(roadmap)) {
|
||||
uniqueRoadmaps.push(roadmap);
|
||||
}
|
||||
}
|
||||
console.log(uniqueRoadmaps); // ['JavaScript', 'React', 'Node.js']
|
||||
```
|
||||
@@ -1,9 +0,0 @@
|
||||
No, the `forEach()` method does not return a new array. It simply calls a provided function on each element in the array.
|
||||
|
||||
```js
|
||||
const roadmaps = ['JavaScript', 'React', 'Node.js'];
|
||||
|
||||
roadmaps.forEach((roadmap) => {
|
||||
console.log(roadmap);
|
||||
});
|
||||
```
|
||||
@@ -1,20 +0,0 @@
|
||||
The Head and Stack in JavaScript Engine are two different data structures that store data in different ways.
|
||||
|
||||
## Stack
|
||||
|
||||
The Stack is a small, organized region of memory. It is where primitive values, function calls, and local variables are stored. It follows a "Last In, First Out" (LIFO) order, meaning that the last item added to the stack is the first one to be removed. Each function invocation creates a new stack frame, which contains the function's local variables, return address, and other contextual data.
|
||||
|
||||
## Heap
|
||||
|
||||
The Heap is a large, mostly unstructured region of memory. It is where `objects`, `arrays`, and `functions` are stored. Variables from the Stack (e.g., in functions) point to locations in the Heap for these dynamically allocated structures.
|
||||
|
||||
When you declare a primitive type (like a number or boolean), it's usually managed in the stack. But when you create an object, array, or function, it's stored in the heap, and the stack will hold a reference to that location in the heap.
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
const name = 'JavaScript'; // Stored on the stack
|
||||
const roadmap = { name: 'JS' }; // `roadmap` reference on the stack, actual object { name: 'JS' } in the heap
|
||||
```
|
||||
|
||||
In the code above, the primitive value `JavaScript` for variable `name` is directly stored on the stack. For the object assigned to `roadmap`, its actual data resides in the heap, and the reference to this data (a memory address pointer) is held on the stack.
|
||||
@@ -1,16 +0,0 @@
|
||||
Hoisting is a JavaScript mechanism where variables and function declarations are moved to the top of their scope before code execution. This means that no matter where the functions and variables are declared, they are moved to the top of their scope regardless of whether their scope is global or local. Note that hoisting only moves the declaration, not the initialization.
|
||||
|
||||
```js
|
||||
console.log(x === undefined); // true
|
||||
var x = 3;
|
||||
console.log(x); // 3
|
||||
```
|
||||
|
||||
The above code snippet can be visualized in the following way:
|
||||
|
||||
```js
|
||||
var x;
|
||||
console.log(x === undefined); // true
|
||||
x = 3;
|
||||
console.log(x); // 3
|
||||
```
|
||||
@@ -1,18 +0,0 @@
|
||||
The IIFE (Immediately Invoked Function Expression) is a JavaScript function that runs as soon as it is defined.
|
||||
|
||||
```js
|
||||
(function () {
|
||||
console.log('Hello Roadmap!');
|
||||
})();
|
||||
```
|
||||
|
||||
The IIFE is frequently used to create a new scope to avoid variable hoisting from within blocks.
|
||||
|
||||
```js
|
||||
(function () {
|
||||
var roadmap = 'JavaScript';
|
||||
console.log(roadmap);
|
||||
})();
|
||||
|
||||
console.log(roadmap); // ReferenceError: name is not defined
|
||||
```
|
||||
@@ -1,12 +0,0 @@
|
||||
To make an object immutable, you can use `Object.freeze()` method. It prevents the modification of existing property values and prevents the addition of new properties.
|
||||
|
||||
```js
|
||||
const roadmap = {
|
||||
name: 'JavaScript',
|
||||
};
|
||||
|
||||
Object.freeze(roadmap);
|
||||
|
||||
roadmap.name = 'JavaScript Roadmap'; // throws an error in strict mode
|
||||
console.log(roadmap.name); // JavaScript
|
||||
```
|
||||
@@ -1,21 +0,0 @@
|
||||
As the name says, the increment operator increases the value of a variable by **1**. There are two types of increment operators: `pre-increment` and `post-increment`.
|
||||
|
||||
## Pre-increment
|
||||
|
||||
The pre-increment operator increases the value of a variable by 1 and then returns the value. For example:
|
||||
|
||||
```js
|
||||
let x = 1;
|
||||
console.log(++x); // 2
|
||||
console.log(x); // 2
|
||||
```
|
||||
|
||||
## Post-increment
|
||||
|
||||
The post-increment operator returns the value of a variable and then increases the value by 1. For example:
|
||||
|
||||
```js
|
||||
let x = 1;
|
||||
console.log(x++); // 1
|
||||
console.log(x); // 2
|
||||
```
|
||||
@@ -1,21 +0,0 @@
|
||||
You can use the `while` or `for` loop to create an infinite loop.
|
||||
|
||||
## While loop
|
||||
|
||||
To create an infinite loop with the `while` loop, we can use the `true` keyword as the condition.
|
||||
|
||||
```js
|
||||
while (true) {
|
||||
// do something
|
||||
}
|
||||
```
|
||||
|
||||
## For loop
|
||||
|
||||
To create an infinite loop with the `for` loop, we can use the `true` keyword as the condition.
|
||||
|
||||
```js
|
||||
for (let i = 0; true; i++) {
|
||||
// do something
|
||||
}
|
||||
```
|
||||
@@ -1,38 +0,0 @@
|
||||
Inheritance is a way to create a new `Class` from an existing `Class`. The new `Class` inherits all the properties and methods from the existing `Class`. The new `Class` is called the child `Class`, and the existing `Class` is called the parent `Class`.
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
class Roadmap {
|
||||
constructor(name, description, slug) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.slug = slug;
|
||||
}
|
||||
|
||||
getRoadmapUrl() {
|
||||
console.log(`https://roadmap.sh/${this.slug}`);
|
||||
}
|
||||
}
|
||||
|
||||
class JavaScript extends Roadmap {
|
||||
constructor(name, description, slug) {
|
||||
super(name, description, slug);
|
||||
}
|
||||
|
||||
greet() {
|
||||
console.log(`${this.name} - ${this.description}`);
|
||||
}
|
||||
}
|
||||
|
||||
const js = new JavaScript(
|
||||
'JavaScript Roadmap',
|
||||
'Learn JavaScript',
|
||||
'javascript'
|
||||
);
|
||||
|
||||
js.getRoadmapUrl(); // https://roadmap.sh/javascript
|
||||
js.greet(); // JavaScript Roadmap - Learn JavaScript
|
||||
```
|
||||
|
||||
In the above example, the `JavaScript` class inherits the `getRoadmapUrl()` method from the `Roadmap` class. This is because the `JavaScript` class extends the `Roadmap` class using the `extends` keyword. In the `JavaScript` class, the `getRoadmapUrl()` method is not found, so JavaScript looks up the prototype chain and finds the `getRoadmapUrl()` method in the `Roadmap` class.
|
||||
@@ -1,15 +0,0 @@
|
||||
JavaScript label statements are used to prefix a label to an identifier. It can be used with `break` and `continue` statement to control the flow more precisely.
|
||||
|
||||
```js
|
||||
loop1: for (let i = 0; i < 5; i++) {
|
||||
if (i === 1) {
|
||||
continue loop1; // skips the rest of the code in the loop1
|
||||
}
|
||||
console.log(`i: ${i}`);
|
||||
}
|
||||
// Output:
|
||||
// i: 0
|
||||
// i: 2
|
||||
// i: 3
|
||||
// i: 4
|
||||
```
|
||||
@@ -1,43 +0,0 @@
|
||||
There are four logical operators in JavaScript: `||` (OR), `&&` (AND), `!` (NOT), and `??` (Nullish Coalescing). They can be used with boolean values, or with non-boolean values.
|
||||
|
||||
## OR (||)
|
||||
|
||||
The OR operator (`||`) returns the first truthy value, or the last value if none are truthy.
|
||||
|
||||
```js
|
||||
console.log('hello' || 0); // hello
|
||||
console.log(false || 'hello'); // hello
|
||||
console.log('hello' || 'world'); // hello
|
||||
```
|
||||
|
||||
## AND (&&)
|
||||
|
||||
The AND operator (`&&`) aka logical conjunction returns the first falsy value, or the last value if none are falsy.
|
||||
|
||||
```js
|
||||
console.log('hello' && 0); // 0
|
||||
console.log(false && 'hello'); // false
|
||||
console.log('hello' && 'world'); // world
|
||||
```
|
||||
|
||||
## NOT (!)
|
||||
|
||||
It simply inverts the boolean value of its operand.
|
||||
|
||||
```js
|
||||
console.log(!true); // false
|
||||
console.log(!false); // true
|
||||
console.log(!'hello'); // false
|
||||
console.log(!0); // true
|
||||
```
|
||||
|
||||
## Nullish Coalescing (??)
|
||||
|
||||
The Nullish Coalescing Operator (`??`) returns the right operand if the left one is `null` or `undefined`, otherwise, it returns the left operand. It's useful for setting default values without considering falsy values like `0` or `''` as absent.
|
||||
|
||||
```js
|
||||
console.log(null ?? 'hello'); // hello
|
||||
console.log(undefined ?? 'hello'); // hello
|
||||
console.log('' ?? 'hello'); // ''
|
||||
console.log(0 ?? 'hello'); // 0
|
||||
```
|
||||
@@ -1,12 +0,0 @@
|
||||
No, the `map()` method does not mutate the original array. It returns a new array with the results of calling a provided function on every element in the calling array.
|
||||
|
||||
```js
|
||||
const roadmaps = ['JavaScript', 'React', 'Node.js'];
|
||||
|
||||
const renamedRoadmaps = roadmaps.map((roadmap) => {
|
||||
return `${roadmap} Roadmap`;
|
||||
});
|
||||
|
||||
console.log(roadmaps); // ['JavaScript', 'React', 'Node.js']
|
||||
console.log(renamedRoadmaps); // ['JavaScript Roadmap', 'React Roadmap', 'Node.js Roadmap']
|
||||
```
|
||||
@@ -1,19 +0,0 @@
|
||||
Map is another data structure in JavaScript which is similar to `Object` but the key can be of any type. It is a collection of elements where each element is stored as a Key, value pair. It is also known as a Hash table or a dictionary.
|
||||
|
||||
The `key` can be of any type but the `value` can be of any type. The `key` is unique and immutable, whereas the `value` can be mutable or immutable.
|
||||
|
||||
```js
|
||||
const roadmap = new Map();
|
||||
roadmap.set('name', 'JavaScript');
|
||||
roadmap.set('type', 'dynamic');
|
||||
roadmap.set('year', 1995);
|
||||
|
||||
console.log(roadmap.get('name')); // JavaScript
|
||||
|
||||
roadmap.delete('year');
|
||||
console.log(roadmap.has('year')); // false
|
||||
console.log(roadmap.size); // 2
|
||||
|
||||
roadmap.clear();
|
||||
console.log(roadmap.size); // 0
|
||||
```
|
||||
@@ -1,8 +0,0 @@
|
||||
You can use `getBoundingClientRect` method to get the dimensions of an element.
|
||||
|
||||
```js
|
||||
const roadmapWrapper = document.querySelector('.roadmap-wrapper');
|
||||
const dimensions = roadmapWrapper.getBoundingClientRect();
|
||||
|
||||
console.log(dimensions); // DOMRect { x: 8, y: 8, width: 784, height: 784, top: 8, right: 792, bottom: 792, left: 8 }
|
||||
```
|
||||
@@ -1,25 +0,0 @@
|
||||
Yes, you can merge multiple arrays into one array using the `concat()` method, or the spread operator `...`.
|
||||
|
||||
## concat()
|
||||
|
||||
The `concat()` method is used to merge two or more arrays. This method does not change the existing arrays, but instead returns a new array.
|
||||
|
||||
```js
|
||||
const arr1 = [1, 2, 3];
|
||||
const arr2 = [4, 5, 6];
|
||||
|
||||
const arr3 = arr1.concat(arr2);
|
||||
console.log(arr3); // [1, 2, 3, 4, 5, 6]
|
||||
```
|
||||
|
||||
## Spread operator
|
||||
|
||||
The spread operator `...` is used to expand an iterable object into the list of arguments.
|
||||
|
||||
```js
|
||||
const arr1 = [1, 2, 3];
|
||||
const arr2 = [4, 5, 6];
|
||||
|
||||
const arr3 = [...arr1, ...arr2];
|
||||
console.log(arr3); // [1, 2, 3, 4, 5, 6]
|
||||
```
|
||||
@@ -1,8 +0,0 @@
|
||||
The Nullish Coalescing Operator (`??`) returns the right operand if the left one is `null` or `undefined`, otherwise, it returns the left operand. It's useful for setting default values without considering falsy values like `0` or `''` as absent.
|
||||
|
||||
```js
|
||||
console.log(null ?? 'hello'); // hello
|
||||
console.log(undefined ?? 'hello'); // hello
|
||||
console.log('' ?? 'hello'); // ''
|
||||
console.log(0 ?? 'hello'); // 0
|
||||
```
|
||||
@@ -1,9 +0,0 @@
|
||||
In order to parse JSON, you can use the `JSON.parse()` method. It parses a JSON string and returns the JavaScript equivalent.
|
||||
|
||||
```js
|
||||
const json = '{"name":"JavaScript","year":1995}';
|
||||
const roadmap = JSON.parse(json);
|
||||
|
||||
console.log(roadmap.name); // JavaScript
|
||||
console.log(roadmap.year); // 1995
|
||||
```
|
||||
@@ -1,10 +0,0 @@
|
||||
The `event.preventDefault()` method is used to prevent the default action of an event. For example, when you click on a link, the default action is to navigate to the link's URL. But, if you want to prevent the navigation, you can use `event.preventDefault()` method.
|
||||
|
||||
```js
|
||||
const link = document.querySelector('a');
|
||||
|
||||
link.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
console.log('Clicked on link!');
|
||||
});
|
||||
```
|
||||
@@ -1,51 +0,0 @@
|
||||
The core difference between `Promise.all()` and `Promise.allSettled()` is that `Promise.all()` rejects immediately if any of the promises reject whereas `Promise.allSettled()` waits for all of the promises to settle (either resolve or reject) and then returns the result.
|
||||
|
||||
## Initialize
|
||||
|
||||
```js
|
||||
const promise1 = Promise.resolve('Promise 1 resolved');
|
||||
const promise2 = Promise.reject('Promise 2 rejected');
|
||||
```
|
||||
|
||||
## Using `Promise.all()`
|
||||
|
||||
```js
|
||||
Promise.all([promise1, promise2])
|
||||
.then((values) => {
|
||||
console.log(values);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('An error occurred in Promise.all():', error);
|
||||
});
|
||||
|
||||
// Output:
|
||||
// An error occurred in Promise.all(): Promise 2 rejected
|
||||
```
|
||||
|
||||
In the above code, the `Promise.all()` rejects immediately when any of the `promise2` rejects.
|
||||
|
||||
## Using `Promise.allSettled()`
|
||||
|
||||
```js
|
||||
Promise.allSettled([promise1, promise2]).then((results) => {
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
console.log(
|
||||
`Promise ${index + 1} was fulfilled with value:`,
|
||||
result.value
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`Promise ${index + 1} was rejected with reason:`,
|
||||
result.reason
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Output:
|
||||
// Promise 1 was fulfilled with value: Promise 1 resolved
|
||||
// Promise 2 was rejected with reason: Promise 2 rejected
|
||||
```
|
||||
|
||||
In the above code, the `Promise.allSettled()` waits for all of the promises to settle (either resolve or reject) and then returns the result.
|
||||
@@ -1,27 +0,0 @@
|
||||
The prototype chain in JavaScript refers to the chain of objects linked by their prototypes. When a property or method is accessed on an object, JavaScript first checks the object itself. If it doesn't find it there, it looks up the property or method in the object's prototype. This process continues, moving up the chain from one prototype to the next, until the property or method is found or the end of the chain is reached (typically the prototype of the base object, which is `null`). The prototype chain is fundamental to JavaScript's prototypal inheritance model, allowing objects to inherit properties and methods from other objects.
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
const roadmap = {
|
||||
getRoadmapUrl() {
|
||||
console.log(`https://roadmap.sh/${this.slug}`);
|
||||
},
|
||||
};
|
||||
|
||||
const javascript = {
|
||||
name: 'JavaScript Roadmap',
|
||||
description: 'Learn JavaScript',
|
||||
slug: 'javascript',
|
||||
greet() {
|
||||
console.log(`${this.name} - ${this.description}`);
|
||||
},
|
||||
};
|
||||
|
||||
Object.setPrototypeOf(javascript, roadmap); // or javascript.__proto__ = roadmap;
|
||||
|
||||
javascript.getRoadmapUrl(); // https://roadmap.sh/javascript
|
||||
javascript.greet(); // JavaScript Roadmap - Learn JavaScript
|
||||
```
|
||||
|
||||
In the above example, the `javascript` object inherits the `getRoadmapUrl()` method from the `roadmap` object. This is because the `javascript` object's prototype is set to the `roadmap` object using the `Object.setPrototypeOf()` method. In the `javascript` object, the `getRoadmapUrl()` method is not found, so JavaScript looks up the prototype chain and finds the `getRoadmapUrl()` method in the `roadmap` object.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user