mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-03-13 10:11:55 +08:00
Compare commits
27 Commits
feat/reado
...
roadmap/de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3e1324b31 | ||
|
|
980e243124 | ||
|
|
044046e044 | ||
|
|
793764c3a3 | ||
|
|
abc8a97676 | ||
|
|
79355cd876 | ||
|
|
2809b81920 | ||
|
|
204a9577cd | ||
|
|
577e724aa7 | ||
|
|
14a1544ed4 | ||
|
|
14ea7ba0ad | ||
|
|
5e7ec4f8d8 | ||
|
|
417badc6ea | ||
|
|
0558957673 | ||
|
|
7f6a42a0c5 | ||
|
|
cc258b7612 | ||
|
|
7da244fe10 | ||
|
|
cf78628c0c | ||
|
|
498e03720f | ||
|
|
5c69b05470 | ||
|
|
309cf3d6d9 | ||
|
|
4f3b891e45 | ||
|
|
47f548a0e4 | ||
|
|
a988ecc4ab | ||
|
|
c723070057 | ||
|
|
3a0e588530 | ||
|
|
d46cf26812 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -29,6 +29,5 @@ pnpm-debug.log*
|
||||
tests-examples
|
||||
*.csv
|
||||
|
||||
/renderer/*
|
||||
!/renderer/index.tsx
|
||||
!/renderer/renderer.ts
|
||||
/editor/*
|
||||
!/editor/readonly-editor.tsx
|
||||
3
.npmrc
3
.npmrc
@@ -1 +1,2 @@
|
||||
auto-install-peers=true
|
||||
auto-install-peers=true
|
||||
strict-peer-dependencies=false
|
||||
@@ -13,6 +13,6 @@ module.exports = {
|
||||
],
|
||||
plugins: [
|
||||
require.resolve('prettier-plugin-astro'),
|
||||
require('prettier-plugin-tailwindcss'),
|
||||
'prettier-plugin-tailwindcss',
|
||||
],
|
||||
};
|
||||
|
||||
14
editor/readonly-editor.tsx
Normal file
14
editor/readonly-editor.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
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,53 +16,54 @@
|
||||
"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.0",
|
||||
"@astrojs/sitemap": "^1.3.3",
|
||||
"@astrojs/tailwind": "^5.0.0",
|
||||
"@fingerprintjs/fingerprintjs": "^3.4.1",
|
||||
"@astrojs/react": "^3.0.3",
|
||||
"@astrojs/sitemap": "^3.0.2",
|
||||
"@astrojs/tailwind": "^5.0.2",
|
||||
"@fingerprintjs/fingerprintjs": "^4.1.0",
|
||||
"@nanostores/react": "^0.7.1",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"astro": "^3.0.5",
|
||||
"astro-compress": "^2.0.8",
|
||||
"@types/react": "^18.2.31",
|
||||
"@types/react-dom": "^18.2.14",
|
||||
"astro": "^3.3.3",
|
||||
"astro-compress": "^2.1.5",
|
||||
"clsx": "^2.0.0",
|
||||
"dracula-prism": "^2.1.13",
|
||||
"jose": "^4.14.4",
|
||||
"jose": "^4.15.4",
|
||||
"js-cookie": "^3.0.5",
|
||||
"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",
|
||||
"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",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-confetti": "^6.1.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"reactflow": "^11.8.3",
|
||||
"rehype-external-links": "^2.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"reactflow": "^11.9.4",
|
||||
"rehype-external-links": "^3.0.0",
|
||||
"roadmap-renderer": "^1.0.6",
|
||||
"slugify": "^1.6.6",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss": "^3.3.3"
|
||||
"tailwindcss": "^3.3.3",
|
||||
"zustand": "^4.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.35.1",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@playwright/test": "^1.39.0",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/js-cookie": "^3.0.5",
|
||||
"@types/prismjs": "^1.26.2",
|
||||
"csv-parser": "^3.0.0",
|
||||
"gh-pages": "^5.0.0",
|
||||
"gh-pages": "^6.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
2305
pnpm-lock.yaml
generated
2305
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
BIN
public/pdfs/roadmaps/game-developer.pdf
Normal file
BIN
public/pdfs/roadmaps/game-developer.pdf
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 561 KiB |
BIN
public/roadmaps/game-developer.png
Normal file
BIN
public/roadmaps/game-developer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 614 KiB |
@@ -39,6 +39,7 @@ 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)
|
||||
|
||||
@@ -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 renderer
|
||||
mkdir renderer
|
||||
rm -rf editor
|
||||
mkdir editor
|
||||
|
||||
# copy the files at /src/editor/renderer/* to /renderer
|
||||
# copy the files at /src/editor/* to /editor
|
||||
# while replacing any existing files
|
||||
cp -rf .temp/web-draw/src/editor/renderer/* renderer
|
||||
cp -rf .temp/web-draw/src/editor/* editor
|
||||
|
||||
# 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 renderer -type f \( -name "*.ts" -o -name "*.tsx" \) -print0 | while IFS= read -r -d '' file; do
|
||||
find editor -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,6 +28,5 @@ find renderer -type f \( -name "*.ts" -o -name "*.tsx" \) -print0 | while IFS= r
|
||||
done
|
||||
|
||||
|
||||
|
||||
# ignore the worktree changes for the renderer directory
|
||||
git update-index --skip-worktree renderer/*
|
||||
# ignore the worktree changes for the editor directory
|
||||
git update-index --assume-unchanged editor/readonly-editor.tsx
|
||||
@@ -19,13 +19,12 @@ if (!allowedRoadmapIds.includes(roadmapId)) {
|
||||
}
|
||||
|
||||
const ROADMAP_CONTENT_DIR = path.join(ALL_ROADMAPS_DIR, roadmapId, 'content');
|
||||
const { Configuration, OpenAIApi } = require('openai');
|
||||
const configuration = new Configuration({
|
||||
const OpenAI = require('openai');
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: OPEN_AI_API_KEY,
|
||||
});
|
||||
|
||||
const openai = new OpenAIApi(configuration);
|
||||
|
||||
function getFilesInFolder(folderPath, fileList = {}) {
|
||||
const files = fs.readdirSync(folderPath);
|
||||
|
||||
@@ -60,16 +59,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 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.`;
|
||||
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.`;
|
||||
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 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.`;
|
||||
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.`;
|
||||
}
|
||||
|
||||
console.log(`Generating '${childTopic || parentTopic}'...`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
openai
|
||||
.createChatCompletion({
|
||||
openai.chat.completions
|
||||
.create({
|
||||
model: 'gpt-4',
|
||||
messages: [
|
||||
{
|
||||
@@ -79,7 +78,7 @@ function writeTopicContent(currTopicUrl) {
|
||||
],
|
||||
})
|
||||
.then((response) => {
|
||||
const article = response.data.choices[0].message.content;
|
||||
const article = response.choices[0].message.content;
|
||||
|
||||
resolve(article);
|
||||
})
|
||||
@@ -92,7 +91,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) {
|
||||
@@ -138,15 +137,14 @@ 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,13 +167,12 @@ 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,6 +8,7 @@ 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;
|
||||
@@ -81,7 +82,8 @@ export function UpdateTeamResourceModal(props: ProgressMapProps) {
|
||||
fontURL: '/fonts/balsamiq.woff2',
|
||||
});
|
||||
|
||||
containerEl.current?.replaceChildren(svg);
|
||||
replaceChildren(containerEl.current!, svg);
|
||||
// containerEl.current?.replaceChildren(svg);
|
||||
|
||||
// Render team configuration
|
||||
removedItems.forEach((topicId: string) => {
|
||||
|
||||
145
src/components/CreateVersion/CreateVersion.tsx
Normal file
145
src/components/CreateVersion/CreateVersion.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
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}
|
||||
>
|
||||
|
||||
@@ -62,7 +62,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 +85,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
|
||||
}),
|
||||
nodes: [],
|
||||
edges: [],
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (error) {
|
||||
@@ -96,9 +96,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;
|
||||
}
|
||||
|
||||
@@ -186,7 +186,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 +213,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,13 +7,12 @@ 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',
|
||||
@@ -121,13 +120,8 @@ export function CustomRoadmap() {
|
||||
return (
|
||||
<>
|
||||
<RoadmapHeader />
|
||||
<RoadmapRenderer roadmap={roadmap!} />
|
||||
<FlowRoadmapRenderer roadmap={roadmap!} />
|
||||
<TopicDetail canSubmitContribution={false} />
|
||||
<UserProgressModal
|
||||
resourceId={roadmap?._id!}
|
||||
resourceType="roadmap"
|
||||
isCustomResource={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import {CircleSlash, PenSquare, Shapes} from 'lucide-react';
|
||||
import { CircleSlash, PenSquare, Shapes } from 'lucide-react';
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
type EmptyRoadmapProps = {
|
||||
roadmapId: string;
|
||||
canManage: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function EmptyRoadmap(props: EmptyRoadmapProps) {
|
||||
const { roadmapId, canManage } = props;
|
||||
const { roadmapId, canManage, className } = props;
|
||||
const editUrl = `${import.meta.env.PUBLIC_EDITOR_APP_URL}/${roadmapId}`;
|
||||
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className={cn('flex h-full items-center justify-center', className)}>
|
||||
<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>
|
||||
@@ -18,9 +20,9 @@ export function EmptyRoadmap(props: EmptyRoadmapProps) {
|
||||
{canManage && (
|
||||
<a
|
||||
href={editUrl}
|
||||
className="mt-4 rounded-md bg-gray-500 px-4 py-2 font-medium text-white hover:bg-gray-600 flex items-center"
|
||||
className="mt-4 flex items-center rounded-md bg-gray-500 px-4 py-2 font-medium text-white hover:bg-gray-600"
|
||||
>
|
||||
<Shapes className="inline-block mr-2 h-4 w-4" />
|
||||
<Shapes className="mr-2 inline-block h-4 w-4" />
|
||||
Edit Roadmap
|
||||
</a>
|
||||
)}
|
||||
|
||||
158
src/components/CustomRoadmap/FlowRoadmapRenderer.tsx
Normal file
158
src/components/CustomRoadmap/FlowRoadmapRenderer.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
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,6 +7,7 @@ import {
|
||||
Globe,
|
||||
LockIcon,
|
||||
Users,
|
||||
PenSquare,
|
||||
} from 'lucide-react';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import {
|
||||
@@ -142,7 +143,7 @@ function CustomRoadmapItem(props: CustomRoadmapItemProps) {
|
||||
|
||||
return (
|
||||
<li
|
||||
className="grid grid-cols-1 p-2.5 sm:grid-cols-[auto_110px]"
|
||||
className="grid grid-cols-1 p-2.5 sm:grid-cols-[auto_172px]"
|
||||
key={roadmap._id!}
|
||||
>
|
||||
<div className="mb-3 grid grid-cols-1 sm:mb-0">
|
||||
@@ -174,10 +175,20 @@ 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-gray-300 bg-white px-2 py-1.5 text-xs hover:bg-gray-50 focus:outline-none'
|
||||
'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'
|
||||
}
|
||||
target={'_blank'}
|
||||
>
|
||||
|
||||
@@ -43,7 +43,7 @@ export function ResourceProgressStats(props: ResourceProgressStatsProps) {
|
||||
<div
|
||||
data-progress-nums-container=""
|
||||
className={cn(
|
||||
'striped-loader relative hidden items-center justify-between bg-white px-2 py-1.5 sm:flex',
|
||||
'striped-loader relative z-50 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 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"
|
||||
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"
|
||||
>
|
||||
<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 z-50 mt-1 w-[140px] rounded-md bg-slate-800 px-2 py-2 text-white shadow-md"
|
||||
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]"
|
||||
>
|
||||
<ul>
|
||||
{onUpdateSharing && (
|
||||
|
||||
@@ -9,6 +9,8 @@ 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 = {};
|
||||
|
||||
@@ -22,9 +24,11 @@ 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() {
|
||||
@@ -65,6 +69,22 @@ 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">
|
||||
@@ -82,7 +102,7 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
|
||||
</span>
|
||||
{team && (
|
||||
<>
|
||||
in
|
||||
from
|
||||
<span className="font-semibold text-gray-900">
|
||||
{team?.name}
|
||||
</span>
|
||||
@@ -117,63 +137,78 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
|
||||
<span className="ml-2">Subscribe</span>
|
||||
</button>
|
||||
</div>
|
||||
{$canManageCurrentRoadmap && (
|
||||
<div className="flex items-center gap-2">
|
||||
{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,
|
||||
});
|
||||
<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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<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>
|
||||
)}
|
||||
{!$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>
|
||||
</div>
|
||||
|
||||
<RoadmapHint
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
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/intent/user?screen_name=kamrify'
|
||||
href='https://twitter.com/kamrify'
|
||||
target='_blank'
|
||||
>
|
||||
<span class='hidden sm:inline'>@kamrify</span>
|
||||
|
||||
@@ -10,6 +10,7 @@ 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;
|
||||
@@ -88,7 +89,8 @@ export class Renderer {
|
||||
});
|
||||
})
|
||||
.then((svg) => {
|
||||
this.containerEl?.replaceChildren(svg);
|
||||
replaceChildren(this.containerEl!, svg);
|
||||
// this.containerEl?.replaceChildren(svg);
|
||||
})
|
||||
.then(() => {
|
||||
return renderResourceProgress(
|
||||
|
||||
@@ -10,6 +10,7 @@ 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;
|
||||
@@ -107,6 +108,25 @@ 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 && (
|
||||
@@ -116,15 +136,7 @@ export function FriendsPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{showFriendProgress && (
|
||||
<UserProgressModal
|
||||
userId={showFriendProgress.friend.userId}
|
||||
resourceId={showFriendProgress.resourceId}
|
||||
resourceType={'roadmap'}
|
||||
onClose={() => setShowFriendProgress(undefined)}
|
||||
isCustomResource={showFriendProgress.isCustomResource}
|
||||
/>
|
||||
)}
|
||||
{showFriendProgress && progressModal}
|
||||
|
||||
<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">
|
||||
|
||||
@@ -23,7 +23,7 @@ function ProgressStatButton(props: ProgressStatButtonProps) {
|
||||
<button
|
||||
disabled={isDisabled}
|
||||
onClick={onClick}
|
||||
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"
|
||||
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"
|
||||
>
|
||||
{icon}
|
||||
<span className="flex flex-grow justify-between">
|
||||
@@ -31,7 +31,7 @@ function ProgressStatButton(props: ProgressStatButtonProps) {
|
||||
<span>{count}</span>
|
||||
</span>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
Restart Asking
|
||||
</span>
|
||||
</button>
|
||||
@@ -62,7 +62,7 @@ export function QuestionFinished(props: QuestionFinishedProps) {
|
||||
<span className="inline sm:hidden">questions</span>
|
||||
</p>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
<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="mt-2 mb-4 sm:mb-0 text-sm">
|
||||
<div className="mb-4 mt-2 text-sm sm:mb-0">
|
||||
<button
|
||||
onClick={() => onReset('reset')}
|
||||
className="flex items-center gap-0.5 text-red-700 hover:text-black text-sm sm:text-base"
|
||||
className="flex items-center gap-0.5 text-sm text-red-700 hover:text-black 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 sm:mb-40 gap-3 text-center">
|
||||
<div className="mb-0 gap-3 text-center sm:mb-40">
|
||||
<QuestionsProgress
|
||||
knowCount={knowCount}
|
||||
didNotKnowCount={dontKnowCount}
|
||||
@@ -241,7 +241,7 @@ export function QuestionsList(props: QuestionsListProps) {
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`flex flex-col gap-1 sm:gap-3 transition-opacity duration-300 sm:flex-row ${
|
||||
className={`flex flex-col gap-1 transition-opacity duration-300 sm:flex-row sm:gap-3 ${
|
||||
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 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"
|
||||
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"
|
||||
>
|
||||
<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 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"
|
||||
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"
|
||||
>
|
||||
<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 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"
|
||||
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"
|
||||
>
|
||||
<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 sm:mb-5 overflow-hidden rounded-lg border border-gray-300 bg-white p-4 sm:p-6">
|
||||
<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 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 mt-6 -mb-6 border-t bg-yellow-100 py-3 text-sm text-yellow-900">
|
||||
<p className="-mx-6 -mb-6 mt-6 border-t bg-yellow-100 py-3 text-sm text-yellow-900">
|
||||
You progress is not saved. Please{' '}
|
||||
<button
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
---
|
||||
import { getQuestionGroupsByIds } from '../lib/question-group';
|
||||
import { getRoadmapsByIds, RoadmapFrontmatter } from '../lib/roadmap';
|
||||
import { Map, Clipboard } from 'lucide-react';
|
||||
|
||||
export interface Props {
|
||||
roadmap: RoadmapFrontmatter;
|
||||
@@ -8,35 +10,89 @@ export interface Props {
|
||||
const { roadmap } = Astro.props;
|
||||
|
||||
const relatedRoadmaps = roadmap.relatedRoadmaps || [];
|
||||
if (!relatedRoadmaps.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const relatedRoadmapDetails = await getRoadmapsByIds(relatedRoadmaps);
|
||||
|
||||
const relatedQuestions = roadmap.relatedQuestions || [];
|
||||
const relatedQuestionDetails = await getQuestionGroupsByIds(relatedQuestions);
|
||||
---
|
||||
|
||||
<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>
|
||||
|
||||
<div class='flex flex-col gap-1 pb-8'>
|
||||
{
|
||||
relatedRoadmapDetails.map((relatedRoadmap) => (
|
||||
{
|
||||
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={`/${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'
|
||||
href='/roadmaps'
|
||||
class='text-md rounded-md border bg-white px-3 py-1 font-medium hover:bg-gray-50'
|
||||
>
|
||||
<span class='font-medium inline-block min-w-[150px]'>{relatedRoadmap.frontmatter.briefTitle}</span>
|
||||
<span class='text-gray-500'>{relatedRoadmap.frontmatter.briefDescription}</span>
|
||||
<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>
|
||||
</div>
|
||||
</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>
|
||||
<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 Roadmaps →</span>
|
||||
<span class='inline sm:hidden'>More →</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import YouTubeAlert from './YouTubeAlert.astro';
|
||||
import ProgressHelpPopup from './ProgressHelpPopup.astro';
|
||||
import { MarkFavorite } from './FeaturedItems/MarkFavorite';
|
||||
import { TeamVersions } from './TeamVersions/TeamVersions';
|
||||
import { RoadmapFrontmatter } from '../lib/roadmap';
|
||||
import { CreateVersion } from './CreateVersion/CreateVersion';
|
||||
import { type RoadmapFrontmatter } from '../lib/roadmap';
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
@@ -20,6 +21,7 @@ export interface Props {
|
||||
hasSearch?: boolean;
|
||||
question?: RoadmapFrontmatter['question'];
|
||||
hasTopics?: boolean;
|
||||
isForkable?: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -32,6 +34,7 @@ const {
|
||||
note,
|
||||
hasTopics = false,
|
||||
question,
|
||||
isForkable = false,
|
||||
} = Astro.props;
|
||||
|
||||
const isRoadmapReady = !isUpcoming;
|
||||
@@ -58,13 +61,21 @@ 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='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'
|
||||
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'
|
||||
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 font-medium text-base"
|
||||
className="z-50 flex cursor-pointer items-center px-2 py-2.5 text-base font-medium"
|
||||
aria-expanded={isAnswerVisible ? 'true' : 'false'}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setIsAnswerVisible(!isAnswerVisible);
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center flex-grow">
|
||||
<span className="flex flex-grow items-center">
|
||||
<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]: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"
|
||||
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"
|
||||
dangerouslySetInnerHTML={{ __html: markdownToHtml(answer, false) }}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { UserItem } from './UserItem';
|
||||
import { Users2 } from 'lucide-react';
|
||||
import {httpGet} from "../../lib/http";
|
||||
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';
|
||||
|
||||
export type FriendshipStatus =
|
||||
| 'none'
|
||||
@@ -41,10 +44,13 @@ 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) {
|
||||
@@ -53,7 +59,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) {
|
||||
@@ -87,6 +93,10 @@ 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) && (
|
||||
@@ -112,32 +122,85 @@ 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]);
|
||||
}
|
||||
<>
|
||||
<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);
|
||||
}}
|
||||
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,
|
||||
},
|
||||
)}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{friends.length === 0 && !isLoading && (
|
||||
@@ -148,7 +211,7 @@ export function ShareFriendList(props: ShareFriendListProps) {
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
href={`${import.meta.env.PUBLIC_ROADMAP_WEB_URL}/account/friends`}
|
||||
href={`/account/friends`}
|
||||
>
|
||||
Invite your friends to share roadmaps with.
|
||||
</a>
|
||||
|
||||
@@ -8,10 +8,17 @@ type ShareSuccessProps = {
|
||||
onClose: () => void;
|
||||
visibility: AllowedRoadmapVisibility;
|
||||
description?: string;
|
||||
isSharingWithOthers?: boolean;
|
||||
};
|
||||
|
||||
export function ShareSuccess(props: ShareSuccessProps) {
|
||||
const { roadmapId, onClose, description, visibility } = props;
|
||||
const {
|
||||
roadmapId,
|
||||
onClose,
|
||||
description,
|
||||
visibility,
|
||||
isSharingWithOthers = false,
|
||||
} = props;
|
||||
|
||||
const baseUrl = import.meta.env.DEV
|
||||
? 'http://localhost:3000'
|
||||
@@ -42,7 +49,11 @@ 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" />
|
||||
<h3 className="text-xl font-medium">Sharing Settings Updated</h3>
|
||||
{isSharingWithOthers ? (
|
||||
<h3 className="text-xl font-medium">Sharing with Others</h3>
|
||||
) : (
|
||||
<h3 className="text-xl font-medium">Sharing Settings Updated</h3>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<input
|
||||
@@ -55,15 +66,21 @@ export function ShareSuccess(props: ShareSuccessProps) {
|
||||
copyText(shareLink);
|
||||
}}
|
||||
/>
|
||||
<p className="mt-1 text-sm text-gray-400">
|
||||
You can share the above link with anyone who has access
|
||||
</p>
|
||||
{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>
|
||||
)}
|
||||
|
||||
{visibility === 'public' && (
|
||||
<>
|
||||
<div className="-mx-4 mt-4 flex items-center gap-1.5">
|
||||
<span className="h-px grow bg-gray-300" />
|
||||
<span className="text-xs uppercase text-gray-400 px-2">Or</span>
|
||||
<span className="px-2 text-xs uppercase text-gray-400">Or</span>
|
||||
<span className="h-px grow bg-gray-300" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CheckCircle, CheckCircle2, CheckIcon } from 'lucide-react';
|
||||
import { CheckCircle } from 'lucide-react';
|
||||
import { isLoggedIn } from '../../lib/jwt.ts';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Check, CheckCircle, Copy, Sparkles } from 'lucide-react';
|
||||
import { Copy } 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!
|
||||
|
||||
294
src/components/TeamProgress/MemberCustomProgressModal.tsx
Normal file
294
src/components/TeamProgress/MemberCustomProgressModal.tsx
Normal file
@@ -0,0 +1,294 @@
|
||||
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,13 +16,8 @@ import CloseIcon from '../../icons/close.svg';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $currentTeam } from '../../stores/team';
|
||||
import { renderFlowJSON } from '../../../renderer/renderer';
|
||||
import {
|
||||
allowedClickableNodeTypes,
|
||||
getNodeDetails,
|
||||
} from '../CustomRoadmap/RoadmapRenderer';
|
||||
import { MemberProgressModalHeader } from './MemberProgressModalHeader';
|
||||
import {replaceChildren} from "../../lib/dom.ts";
|
||||
|
||||
export type ProgressMapProps = {
|
||||
member: TeamMember;
|
||||
@@ -49,7 +44,6 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
onShowMyProgress,
|
||||
teamId,
|
||||
onClose,
|
||||
isCustomResource,
|
||||
} = props;
|
||||
const user = useAuth();
|
||||
const isCurrentUser = user?.email === member.email;
|
||||
@@ -70,12 +64,6 @@ 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,
|
||||
@@ -98,30 +86,14 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
}
|
||||
|
||||
async function renderResource(jsonUrl: string) {
|
||||
const res = await fetch(jsonUrl, {
|
||||
...(isCustomResource && {
|
||||
credentials: 'include',
|
||||
}),
|
||||
});
|
||||
const res = await fetch(jsonUrl, {});
|
||||
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',
|
||||
});
|
||||
}
|
||||
const svg: SVGElement | null = await wireframeJSONToSVG(json, {
|
||||
fontURL: '/fonts/balsamiq.woff2',
|
||||
});
|
||||
|
||||
containerEl.current?.replaceChildren(svg);
|
||||
replaceChildren(containerEl.current!, svg);
|
||||
// containerEl.current?.replaceChildren(svg);
|
||||
}
|
||||
|
||||
useKeydown('Escape', () => {
|
||||
@@ -215,29 +187,11 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
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 groupId = targetGroup.dataset ? targetGroup.dataset.groupId : '';
|
||||
if (!groupId) {
|
||||
return;
|
||||
}
|
||||
const topicId = groupId.replace(/^\d+-/, '');
|
||||
|
||||
if (targetGroup.classList.contains('removed')) {
|
||||
e.preventDefault();
|
||||
@@ -255,29 +209,11 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
if (!targetGroup) {
|
||||
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 groupId = targetGroup.dataset ? targetGroup.dataset.groupId : '';
|
||||
if (!groupId) {
|
||||
return;
|
||||
}
|
||||
const topicId = groupId.replace(/^\d+-/, '');
|
||||
|
||||
if (targetGroup.classList.contains('removed')) {
|
||||
return;
|
||||
@@ -321,136 +257,24 @@ 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={isCustomResource ? 'original-roadmap' : 'customized-roadmap'}
|
||||
id={'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"
|
||||
>
|
||||
{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>
|
||||
<MemberProgressModalHeader
|
||||
resourceId={resourceId}
|
||||
member={member}
|
||||
progress={memberProgress}
|
||||
isCurrentUser={isCurrentUser}
|
||||
onShowMyProgress={onShowMyProgress}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
<div
|
||||
id={'resource-svg-wrap'}
|
||||
|
||||
148
src/components/TeamProgress/MemberProgressModalHeader.tsx
Normal file
148
src/components/TeamProgress/MemberProgressModalHeader.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
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,6 +9,7 @@ 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;
|
||||
@@ -152,10 +153,15 @@ export function TeamProgressPage() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ProgressModal =
|
||||
showMemberProgress && !showMemberProgress.isCustomResource
|
||||
? MemberProgressModal
|
||||
: MemberCustomProgressModal;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showMemberProgress && (
|
||||
<MemberProgressModal
|
||||
<ProgressModal
|
||||
member={showMemberProgress.member}
|
||||
teamId={teamId}
|
||||
resourceId={showMemberProgress.resourceId}
|
||||
|
||||
@@ -27,6 +27,7 @@ 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();
|
||||
@@ -428,7 +429,12 @@ export function TeamRoadmaps() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="grid grid-cols-1 p-2.5 sm:grid-cols-[auto_110px]"
|
||||
className={cn(
|
||||
'grid grid-cols-1 p-2.5',
|
||||
canManageCurrentTeam
|
||||
? 'sm:grid-cols-[auto_172px]'
|
||||
: 'sm:grid-cols-[auto_110px]'
|
||||
)}
|
||||
key={resourceConfig.resourceId}
|
||||
>
|
||||
<div className="mb-3 grid grid-cols-1 sm:mb-0">
|
||||
@@ -479,6 +485,18 @@ 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 { useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, 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,6 +184,10 @@ export function TopicDetail(props: TopicDetailProps) {
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) topicRef?.current?.focus();
|
||||
}, [isActive]);
|
||||
|
||||
if (!isActive) {
|
||||
return null;
|
||||
}
|
||||
@@ -194,7 +198,8 @@ export function TopicDetail(props: TopicDetailProps) {
|
||||
<div className={'relative z-50'}>
|
||||
<div
|
||||
ref={topicRef}
|
||||
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"
|
||||
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"
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="flex w-full justify-center">
|
||||
@@ -279,7 +284,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() +
|
||||
|
||||
37
src/components/UserProgress/ProgressLoadingError.tsx
Normal file
37
src/components/UserProgress/ProgressLoadingError.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
218
src/components/UserProgress/UserCustomProgressModal.tsx
Normal file
218
src/components/UserProgress/UserCustomProgressModal.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
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,9 +9,8 @@ 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 { Spinner } from '../ReactIcons/Spinner';
|
||||
import { ErrorIcon } from '../ReactIcons/ErrorIcon';
|
||||
import { renderFlowJSON } from '../../../renderer/renderer';
|
||||
import { ProgressLoadingError } from './ProgressLoadingError';
|
||||
import { UserProgressModalHeader } from './UserProgressModalHeader';
|
||||
|
||||
export type ProgressMapProps = {
|
||||
userId?: string;
|
||||
@@ -21,7 +20,7 @@ export type ProgressMapProps = {
|
||||
isCustomResource?: boolean;
|
||||
};
|
||||
|
||||
type UserProgressResponse = {
|
||||
export type UserProgressResponse = {
|
||||
user: {
|
||||
_id: string;
|
||||
name: string;
|
||||
@@ -40,7 +39,6 @@ export function UserProgressModal(props: ProgressMapProps) {
|
||||
resourceType,
|
||||
userId: propUserId,
|
||||
onClose: onModalClose,
|
||||
isCustomResource,
|
||||
} = props;
|
||||
|
||||
const { s: userId = propUserId } = getUrlParams();
|
||||
@@ -69,12 +67,6 @@ 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,
|
||||
@@ -101,12 +93,6 @@ 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',
|
||||
});
|
||||
@@ -180,14 +166,6 @@ 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);
|
||||
})
|
||||
@@ -199,16 +177,6 @@ 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;
|
||||
@@ -219,31 +187,7 @@ export function UserProgressModal(props: ProgressMapProps) {
|
||||
}
|
||||
|
||||
if (isLoading || 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 <ProgressLoadingError isLoading={isLoading} error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -256,62 +200,10 @@ export function UserProgressModal(props: ProgressMapProps) {
|
||||
ref={popupBodyEl}
|
||||
className={`popup-body relative rounded-lg bg-white pt-[1px] shadow`}
|
||||
>
|
||||
<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>
|
||||
<UserProgressModalHeader
|
||||
isLoading={isLoading}
|
||||
progressResponse={progressResponse}
|
||||
/>
|
||||
|
||||
<div
|
||||
ref={resourceSvgEl}
|
||||
|
||||
79
src/components/UserProgress/UserProgressModalHeader.tsx
Normal file
79
src/components/UserProgress/UserProgressModalHeader.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
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.
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -5,13 +5,17 @@ briefTitle: 'Android'
|
||||
briefDescription: 'Step by step guide to becoming an Android Developer in 2023'
|
||||
title: 'Android Developer'
|
||||
description: 'Step by step guide to becoming an Android developer in 2023'
|
||||
hasTopics: false
|
||||
hasTopics: true
|
||||
isNew: true
|
||||
dimensions:
|
||||
width: 968
|
||||
height: 2197.76
|
||||
schema:
|
||||
headline: 'Android Developer Roadmap'
|
||||
description: 'Learn how to become a Android Developer with this interactive step by step guide in 2023. We also have resources and short descriptions attached to the roadmap items so you can get everything you want to learn in one place.'
|
||||
imageUrl: 'https://roadmap.sh/roadmaps/android.png'
|
||||
datePublished: '2023-01-05'
|
||||
dateModified: '2023-01-20'
|
||||
datePublished: '2023-01-24'
|
||||
dateModified: '2023-10-24'
|
||||
seo:
|
||||
title: 'Android Developer Roadmap: Learn to become an Android developer'
|
||||
description: 'Community driven, articles, resources, guides, interview questions, quizzes for android development. Learn to become a modern Android developer by following the steps, skills, resources and guides listed in this roadmap.'
|
||||
@@ -28,10 +32,8 @@ seo:
|
||||
- 'android quiz'
|
||||
- 'android interview questions'
|
||||
relatedRoadmaps:
|
||||
- 'frontend'
|
||||
- 'javascript'
|
||||
- 'react'
|
||||
- 'nodejs'
|
||||
- 'react-native'
|
||||
- 'flutter'
|
||||
sitemap:
|
||||
priority: 1
|
||||
changefreq: 'monthly'
|
||||
@@ -39,104 +41,4 @@ tags:
|
||||
- 'roadmap'
|
||||
- 'main-sitemap'
|
||||
- 'role-roadmap'
|
||||
---
|
||||
The intent of this guide is to give you an idea about the Android development landscape and to help guide your learning if you are confused. Before we start, please note that the roadmap is opinionated, and you might have different opinions than those of the author. Having said that, [we would love to hear your opinions](https://github.com/kamranahmedse/developer-roadmap/issues/new) and incorporate them in the roadmap if suitable.
|
||||
|
||||
There are multiple ways to develop applications for Android; you can go down the path of hybrid application development where [Flutter](https://flutter.dev/), [React-Native](https://reactnative.dev/), or [NativeScript](https://www.nativescript.org/) are the most common contenders. Flutter uses Dart, whereas React Native and Native Script rely on JavaScript. Answering the question of hybrid vs native is out of the scope of this roadmap. This roadmap is focused on native Android app development, but if you are interested in learning any hybrid framework, my personal preference is [React-Native](https://reactnative.dev) and I would recommend you check out the [Frontend Developer Roadmap](/frontend).
|
||||
|
||||
## Complete Roadmap
|
||||
|
||||
Here is the full version of the roadmap in a single image and after that we have the broken down version with the resources and links to learn more about each of the boxes.
|
||||
|
||||
[](/roadmaps/android/roadmap.png)
|
||||
|
||||
## Broken Down Version
|
||||
|
||||
Below is the broken down version of the roadmap with links and resources to learn more about each of the items listed in the complete roadmap above.
|
||||
|
||||
## Pick a Language
|
||||
|
||||
For the languages, you can develop Android apps either by using Kotlin or Java.
|
||||
|
||||
[](/roadmaps/android/pick-language.svg)
|
||||
|
||||
Although, you can use both [Kotlin](<https://en.wikipedia.org/wiki/Kotlin_(programming_language)>) and [Java](<https://en.wikipedia.org/wiki/Java_(programming_language)>) to develop native Android apps, [Google announced in 2019](https://android-developers.googleblog.com/2019/05/google-io-2019-empowering-developers-to-build-experiences-on-Android-Play.html) to make Kotlin the preferred way of developing Android applications. If you were to start learning Android development today, Kotlin should be your language of choice.
|
||||
|
||||
## The Fundamentals
|
||||
|
||||
Install [Android Studio](https://developer.android.com/studio) and learn the basics of Kotlin to get started.
|
||||
|
||||
[](/roadmaps/android/the-fundamentals.png)
|
||||
|
||||
We have also listed down some free resources which you can use for the items listed in the image above. If you have some better ones, please do suggest. Also, you don't need to go through all of them, just go through them and pick what you like.
|
||||
|
||||
- [Learn the basics of Kotlin](https://blog.teamtreehouse.com/absolute-beginners-guide-kotlin)
|
||||
- [Kotlin Docs](https://kotlinlang.org/docs/reference/basic-syntax.html) and [Official Kotlin Tutorials](https://kotlinlang.org/docs/tutorials/)
|
||||
- [Data Structures and Algorithms](https://www.studytonight.com/data-structures/introduction-to-data-structures). Also [check this](https://www.tutorialspoint.com/data_structures_algorithms/index.htm).
|
||||
- [Kotlin Data Structures](https://kotlinlang.org/docs/reference/collections-overview.html)
|
||||
- [Algorithms and Data Structures in Kotlin](https://github.com/bmaslakov/kotlin-algorithm-club)
|
||||
- [Getting started with Gradle](https://docs.gradle.org/current/userguide/userguide.html)
|
||||
Note: Android Studio comes with a working installation of Gradle, so you don’t need to install Gradle separately in that case.
|
||||
|
||||
## Version Control Systems
|
||||
|
||||
Version Control Systems record your changes to the codebase and allow you to recall specific versions later. There are multiple Version Control Systems available, but [Git](https://git-scm.com/) is the most common one these days.
|
||||
|
||||
[](/roadmaps/android/git-github.png)
|
||||
|
||||
Here are some of the resources to get you started. Feel free to google and find something else that you find easier.
|
||||
|
||||
- [Udacity — Version Control with Git](https://www.udacity.com/course/version-control-with-git--ud123)
|
||||
- [GitHub Hello World](https://guides.github.com/activities/hello-world/)
|
||||
|
||||
## Building an Application
|
||||
|
||||
Here is the list of items that you are going to need when developing Android applications. Please note that this is an exhaustive list, and you don't need to know it all from the get-go. Get an idea of the items listed, and just start building some apps and keep the items listed in the back of your mind and have a deep dive when using them.
|
||||
|
||||
[](/roadmaps/android/build-an-application.png)
|
||||
|
||||
To learn more about the items listed in the image above, here are the links to the relevant docs.
|
||||
|
||||
- [Using Activities and Activity Life Cycles](https://developer.android.com/guide/components/activities/intro-activities)
|
||||
- Building Flexible Interfaces using [Fragments](https://developer.android.com/guide/components/fragments)
|
||||
- [Debugging using Android Studio Debugger](https://developer.android.com/studio/debug)
|
||||
- [Handling App Configurations](https://developer.android.com/work/managed-configurations)
|
||||
- [Using Intents and Intent Filters](https://developer.android.com/guide/components/intents-filters)
|
||||
- [Understand Context](https://guides.codepath.com/android/Using-Context)
|
||||
- [Learn about Multithreading](https://developer.android.com/training/multiple-threads)
|
||||
- [Data Privacy](https://www.raywenderlich.com/6901838-data-privacy-for-android)
|
||||
- [Securing Network Data](https://www.raywenderlich.com/5634-securing-network-data-tutorial-for-android)
|
||||
- [Dependency Injection](https://developer.android.com/training/dependency-injection)
|
||||
- [Content Providers](https://developer.android.com/guide/topics/providers/content-providers)
|
||||
- [Glide](https://github.com/bumptech/glide), [Retrofit](https://square.github.io/retrofit/), [Crashlytics](https://firebase.google.com/docs/crashlytics/get-started), [GSON](https://github.com/google/gson)
|
||||
- [Room](https://developer.android.com/topic/libraries/architecture/room), [Navigation](https://developer.android.com/guide/navigation/navigation-getting-started), [Work Manager](https://developer.android.com/topic/libraries/architecture/workmanager), [LiveData](https://developer.android.com/topic/libraries/architecture/livedata), [Data Binding](https://developer.android.com/topic/libraries/data-binding)
|
||||
- [RxJava](https://github.com/ReactiveX/RxJava), [RxKotlin](https://github.com/ReactiveX/RxKotlin)
|
||||
- [Memory Management Overview](https://developer.android.com/topic/performance/memory-overview)
|
||||
- [Diving deeper into context-oriented programming in Kotlin](https://proandroiddev.com/diving-deeper-into-context-oriented-programming-in-kotlin-3ecb4ec38814)
|
||||
|
||||
## Jetpack Compose
|
||||
|
||||
Jetpack Compose is Android’s modern toolkit for building native UI. It simplifies and accelerates UI development on Android. Quickly bring your app to life with less code, powerful tools, and intuitive Kotlin APIs.
|
||||
|
||||
- [Jetpack Compose](https://developer.android.com/jetpack/compose/documentation)
|
||||
- [Material Design 3](https://m3.material.io/)
|
||||
- [Getting started with Material Components](https://m3.material.io/libraries/mdc-android/getting-started)
|
||||
|
||||
## Free Resources
|
||||
|
||||
I would highly recommend watching [this free course](https://www.udacity.com/course/developing-android-apps-with-kotlin--ud9012) from Google on Developing Android Apps with Kotlin. You may also get started with this [free course](https://developer.android.com/courses/android-basics-kotlin/course) on the Android developer's page, where concepts are taught with the help of code labs, projects and quizzes, and you also earn badges as you learn that appear on your Google developer profile. Also, here are some of the resources to learn more about the topics listed above.
|
||||
|
||||
- [Developing Android Apps with Kotlin](https://www.udacity.com/course/developing-android-apps-with-kotlin--ud9012)
|
||||
- [Android Basics in Kotlin](https://developer.android.com/courses/android-basics-kotlin/course)
|
||||
- [Android Developer Guides](https://developer.android.com/guide)
|
||||
- [Kodeco](https://www.kodeco.com)
|
||||
|
||||
## Wrap Up
|
||||
|
||||
That wraps it up for the Android developer roadmap. Again, remember to not be exhausted by the list; just learn the basics and start working on some project and the rest of the learnings will come along the way. Good luck!
|
||||
|
||||
For any suggestions, improvements and feedback, feel free to [submit an issue](https://github.com/kamranahmedse/developer-roadmap) or reach out to me on twitter [@kamrify](https://twitter.com/kamrify).
|
||||
|
||||
<!-- @fixme add padding to the container -->
|
||||
|
||||
<br /><br /><br />
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
# Kotlin
|
||||
|
||||
`Kotlin` is a cross-platform, statically typed general-purpose programming language with type inference. Developed by JetBrains, the makers of the world’s leading IDEs, Kotlin has a syntax, which is more expressive and concise. This allows for more readable and maintainable code. It is fully interoperable with Java and comes with no limitations. It can be used almost everywhere Java is used today, for server-side development, Android apps, and much more. Kotlin introduces several improvements for programmers over Java, which makes it a preferred choice for many developers. With more concise code base and modern programming concept support - it's certainly a future of Android app development.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Java
|
||||
|
||||
Java is a popular programming language used for Android development due to its robustness and ease of use. Its object-oriented structure allows developers to create modular programs and reusable code. The language was built with the philosophy of "write once, run anywhere" (WORA), meaning compiled Java code can run on all platforms without the need for recompilation. Android’s API and core libraries are primarily written in Java, therefore understanding Java is fundamental in creating diverse and powerful Android apps. Java is a statically-typed language, which can be beneficial for detecting errors at compile-time rather than at runtime. Oracle, who owns Java, provides comprehensive documentation and support for the language.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Pick a Language
|
||||
|
||||
When developing for Android, one crucial step is picking a programming language to use. There are multiple languages you can choose from, but the three most popular ones are Java, Kotlin, and C++. Java is the original language used for Android development and is widely used, making it a good choice for beginners due to the wealth of resources and developer communities. Kotlin is a newer option that is fully supported by Google and Android Studio, and addressing many of the drawbacks of Java which makes it a popular choice for many developers. Lastly, C++ can be used in Android development through the Android Native Development Kit (NDK), though it comes with more complexities and is usually not recommended for beginners. Your selection might depend on your existing familiarity with these languages, the complexity and specific requirements of your project, and the resources or libraries you wish to use.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Development IDE
|
||||
|
||||
"Development IDE" refers to Development Integrated Development Environment that is vital for Android App development. For Android, the primary IDE is **Android Studio**. This official IDE from Google includes everything you need to build an Android app, such as a code editor, code analysis tools, emulators for all of Android's supported OS versions and hardware configurations, and more. Other popular IDEs include **Eclipse** (with an Android Developer Tools plugin), **NetBeans**, and **IntelliJ IDEA**. Each of these IDEs tends to have its own set of specialized features, but all are designed to provide the tools and services needed for Android development. The choice of IDE often depends on the specific needs and preferences of the developer or development team.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Basics of Kotlin
|
||||
|
||||
Kotlin is a statically-typed programming language that runs on the Java Virtual Machine (JVM) and can be used to develop all types of Android apps. It is Google's preferred language for Android app development. Kotlin's syntax is more concise than Java, which means less code to write and read, and fewer opportunities for errors. It provides several high-level features like lambdas, coroutines and higher order functions that help making the code more clean and understandable. Key basics of Kotlin include control flow statements (if, when, for, while), variables (mutable and non-mutable), null safety, classes and objects, inheritance, interfaces, and exception handling. While learning Kotlin, experience with Java will certainly be helpful, but it's not a prerequisite.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Basics of OOP
|
||||
|
||||
In Android development, understanding the `Basics of Object-Oriented Programming (OOP)` is crucial. OOP is a programming paradigm that uses "Objects" - entities that contain both data and functions that manipulate the data. Key concepts include `Classes`, which are blueprints from which objects are created; `Objects`, instances of a class; `Inheritance`, where one class acquires properties from another; `Polymorphism`, the ability of an object to take many forms; `Abstraction`, showing only necessary details and hiding implementation from the user; and `Encapsulation`, the concept of wrapping data and the methods that work on data within one unit. By understanding these fundamentals, you can create more efficient and effective Android apps.
|
||||
@@ -0,0 +1,5 @@
|
||||
# DataStructures and Algorithms
|
||||
|
||||
In Android, **data structures** are primarily used to collect, organize and perform operations on the stored data more effectively. They are essential for designing advanced-level Android applications. Examples include Array, Linked List, Stack, Queue, Hash Map, and Tree.
|
||||
|
||||
Meanwhile, **algorithms** are a sequence of instructions or rules for performing a particular task. In Android, algorithms can be used for data searching, sorting, or performing complex business logic. Some commonly used algorithms are Binary Search, Bubble Sort, Selection Sort, etc. A deep understanding of data structures and algorithms is crucial in optimizing the performance and the memory consumption of the Android applications.
|
||||
@@ -0,0 +1,3 @@
|
||||
# What is and how to use Gradle?
|
||||
|
||||
**Using Gradle**: Gradle is a powerful build system used in Android development that allows you to define your project and dependencies, and distinguish between different build types and flavors. Gradle uses a domain-specific language (DSL) which gives developers almost complete control over the build process. When you trigger a build in Android Studio, Gradle is the tool working behind the scenes to compile and package your app. It looks at the dependencies you declared in your build.gradle files and creates a build script accordingly. Using Gradle in android development requires continuous editing of the build.gradle files to manage app dependencies, build variants, signing configurations and other essential aspects related to building your app.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Create a basic Hello World App
|
||||
|
||||
The "Hello World" app is a simple project that you can build when you're getting started with Android development. It's often the first program that beginners learn to build in a new system. It's usually considered the simplest form of program that displays a message to the user - "Hello, World!" In Android, this involves creating a new project from the Android Studio and setting up the main activity. The main activity file is primarily written in Java or Kotlin where you can code for the display message, while the layout design view can be created in the XML file.
|
||||
@@ -0,0 +1,3 @@
|
||||
# The Fundamentals
|
||||
|
||||
"The Fundamentals" of Android primarily concentrate on 5 components; Activities, Services, Broadcast Receivers, Content Providers, and Intents. **Activities** are essentially what you see on your screen; each screen in an app is a separate activity. **Services** run in the background to perform long-running operations or to perform work for remote processes. They do not provide a user interface. **Broadcast Receivers** respond to broadcast messages from other applications or from the system itself. These messages are often in the form of Intents. **Content Providers** manage a shared set of app data that other apps can query or modify, through a structured interface. Finally, **Intents** are messaging objects which facilitate the communication between the aforementioned components. Understanding these five core concepts is key to mastering Android fundamentals.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Git
|
||||
|
||||
`Git` is a highly efficient and flexible distributed version control system that was created by Linus Torvalds, the creator of Linux. It allows multiple developers to work on a project concurrently, providing tools for non-linear development and tracking changes in any set of files. Git has a local repository with a complete history and version-tracking capabilities, allowing offline operations, unlike SVN. It ensures data integrity and provides strong support for non-linear development with features such as branching and merging. Yet, Git has a high learning curve and can be complex for beginners to understand the command line interface. Furthermore, Git also allows you to create `tags` to reference certain points in your history for milestone or version releases.
|
||||
@@ -0,0 +1,4 @@
|
||||
# GitHub
|
||||
|
||||
**GitHub** is a cloud-based hosting service for managing software version control using Git. It provides a platform for enabling multiple developers to work together on the same project at the same time. With GitHub, codes can be stored publicly, allowing for collaboration with other developers or privately for individual projects. Key features of GitHub include code sharing, task management, and version control, among others. GitHub also offers functionalities such as bug tracking, feature requests, and task management for the project.
|
||||
For Android development, it supports Gradle-based android projects, plugins for Android Studio and JetBrains IntelliJ IDEA, making version control operations more user-friendly.
|
||||
@@ -0,0 +1,3 @@
|
||||
# BitBucket
|
||||
|
||||
Bitbucket is a web-based hosting service that is owned by Atlassian. Bitbucket uses either Mercurial or Git revision control systems, allowing users to manage and maintain their code. This platform is mainly used for code and code review. Bitbucket provides both commercial plans and free accounts. It offers free accounts with an unlimited number of private repositories (which can have up to five users in the case of free accounts) as of September 2010. It originally offered only Mercurial support. Bitbucket integrates with other Atlassian software like JIRA, HipChat, Confluence and Bamboo.
|
||||
@@ -0,0 +1,3 @@
|
||||
# GitLab
|
||||
|
||||
`Gitlab` is a web-based DevOps lifecycle tool which provides a Git-repository manager, along with continuous integration and deployment pipeline features, using an open-source license, developed by GitLab Inc. Users can manage and create their software projects and repositories, and collaborate on these projects with other members. `Gitlab` also allows users to view analytics and open issues of their project. It stands next to other version control tools like `GitHub` and `Bitbucket`, but comes with its own set of additional features and nuances. For Android development, `Gitlab` can be particularly useful owing to its continuous integration and deployment system which can automate large parts of the app testing and deployment.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Version Control Systems
|
||||
|
||||
_Version Control_ is a system that records changes to a file or set of files over time so that you can recall specific versions later. An essential tool for software development, it helps to track changes, enhance collaboration, and manage different versions of a project. Two common types of version control systems are Centralized Version Control System (CVCS) and Distributed Version Control System (DVCS). CVCS uses a central server to store all versions of a project, with users getting snapshots from that server. Examples include SVN and Perforce. On the other hand, DVCS allows multiple developers to work on a single project simultaneously. Each user has a complete backup of all versions of the work. Examples include Git and Mercurial.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Activity Lifecycle
|
||||
|
||||
The **Activity Lifecycle** in Android represents a series of states or events that an activity can go through from its creation to its destruction. The primary states or events are `onCreate()`, `onStart()`, `onResume()`, `onPause()`, `onStop()`, `onDestroy()`, and `onRestart()`. The method `onCreate()` is called when the activity is first created, followed by `onStart()` when the activity becomes visible to the user. The `onResume()` method executes when the user starts interacting with the application. `onPause()` and `onStop()` methods are invoked when the application is no longer in the foreground or visible to the user. The `onDestroy()` method is used when the activity is being completely removed from the memory. The `onRestart()` method is called after the system stops the activity and is about to start it again. The proper handling of these states ensures the efficient use of resources and a smooth user experience.
|
||||
@@ -0,0 +1,3 @@
|
||||
# State Changes
|
||||
|
||||
In Android, an "Activity" is a crucial component that represents a single screen with a user interface. One or more active activities make up an Application. These activities can go through different states in their lifecycle, often due to user interaction or system interruption. The primary states of an Activity include `Created`, `Started`, `Resumed`, `Paused`, `Stopped`, `Restarted`, and `Destroyed`. The "Created" state occurs when an activity instance is being created. The "Started" state is when the activity is visible to the user, while "Resumed" is when the activity is interacting with the user. An activity is "Paused" when it loses focus but is partly visible, "Stopped" when it's not visible, "Restarted" when the activity is about to be started, and "Destroyed" when the activity is finished or the system is temporarily destroying it.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Tasks and Backstack
|
||||
|
||||
The **tasks backstack** in Android refers to the way Android manages and arranges tasks in a stack-like structure. Every task has a stack of activities, which is referred to as the task's back stack. The activities are placed in the order they are opened. When a new activity is started, it is placed at the top of the stack and becomes the running activity, while the previous activity is paused and put into the back stack. When you press the back button, the current activity is destroyed and the activity at the top of the back stack becomes active again. Android defines how to navigate between tasks and activities using this back stack concept.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Activity
|
||||
|
||||
`Activity` in Android is a crucial component that represents a single screen with a user interface. It is just like a window in a desktop application. Android apps are typically made up of one or more activities, each having its interface which allows user interaction. When an app is launched, an instance of `Activity` is created, starting the lifecycle of that app. Every activity has its own lifecycle (create, start, resume, pause, stop, destroy) that keeps the state of a user's progress, and Android manages these states automatically. Activities can also have `Intent`, which allows them to interact with other components, such as starting another activity or getting a result from that activity.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Services
|
||||
|
||||
**Services**: A service in Android is an app component that performs operations in the background without a user interface. It can be started by an application component, like an activity, and it will continue to run in the background even if the user switches to another application. There are two types of services in Android, namely, `Started Service` and `Bound Service`. A `Started Service` is used to perform a single operation, such as downloading a large file. On the other hand, a `Bound Service` offers a client-server interface that allows components to interact with the service, send requests, receive results, and even perform interprocess communication (IPC).
|
||||
@@ -0,0 +1,3 @@
|
||||
# Content Provider
|
||||
|
||||
A **Content Provider** in Android is a key component that allows applications to securely share data with other applications. They act as a layer between databases and applications to enhance data security. Content providers manage access to a structured set of data by handling data transactions, implementing data security, and maintaining isolation between applications. They provide an abstracted interface which is used to access data, while the underlying storage method (Like SQLite database, web, or any other method) remains hidden. This mechanism aids in retrieving data from a non-relational source in a structured way. They're used primarily when data needs to be shared between multiple applications, not just within a single application.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Broadcast Receiver
|
||||
|
||||
**Broadcast Receivers** in Android are components that respond to system-wide broadcast announcements. They can be registered to respond to a specific type of broadcasts or implement a user-defined broadcast. While you can initiate a broadcast from your app, they are generally used for receiving system notifications or communicating with other applications. However, keep in mind that they cannot display a user interface, but they can start activities if necessary, which do have a user interface. A `BroadcastReceiver` class must override the `onReceive()` method where each message is received as an `Intent` object parameter.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Implicit Intents
|
||||
|
||||
In Android development, **Implicit Intents** do not specify the target component explicitly like Explicit Intents. Instead, they allow the system to find a suitable component matching the Intent description to handle the request. The system will find an activity that can handle this intent by comparing the `<intent-filter>` section in the `AndroidManifest.xml` of all apps installed on the device against the Implicit Intent. An ideal example of an implicit intent is opening a URL. You do not need to know the specific activity that can handle this request, you just declare an intent to view a web page and Android system will select the suitable app that can open the URL.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Explicit Intents
|
||||
|
||||
**Explicit Intents** are primarily used within an application's own boundaries. In explicit intents you specify the component that needs to be responded to the intent. Therefore, the target component must be specified by calling methods such as `setComponent(ComponentName)`, `setClass(Context, Class)`, or `setClassName(String, String)`. This means that explicit intents are typically used for launching activities, broadcasting messages, starting services within the app. Explicit intents are not resolved by the system but are passed to the component identified in the intent.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Intent Filters
|
||||
|
||||
`Intent Filters` in Android are essential components of the Android system where you can declare the capabilities of your activities, services, and broadcast receivers. An intent filter is an expression found in your app's manifest file, defined in the <intent-filter> XML element. Android uses these filters to determine the appropriate components for incoming intents, which can be either explicit or implicit. Your app's ability to respond to intents depends on the filters you define. The filters are set of conditions comprised of `action`, `category`, and `data` which your activity or service is able to perform. If the incoming `Intent` matches with defined `Intent Filters`, Android system will permit that `Intent` to your Component (Activity, Service, or Broadcast Receiver).
|
||||
@@ -0,0 +1,3 @@
|
||||
# Intent
|
||||
|
||||
"Intent" in Android is a software mechanism used for late runtime binding between components, such as activities, content providers, and services. It is essentially a passive data structure holding an abstract description of an operation that the Android system is requested to perform. The Intent can be explicit, in which you specify the component to start or implicit, where you declare a general action to perform, allowing a component from another app to handle it. Implicit intents are often used to request another app's functionality, such as showing a user a location on a map or taking a photo. "Intent Filters" are then used by the components to advertise their capabilities to handle different types of intents.
|
||||
@@ -0,0 +1,15 @@
|
||||
# App Components
|
||||
|
||||
Android apps are primarily made up of five different types of components:
|
||||
|
||||
1. **Activities**: These are individual screens that a user can interact with. Any UI action like touching a button or swiping a screen will usually take place within an activity.
|
||||
|
||||
2. **Services**: Unlike activities, services run in the background and don't have a user interface. They’re used for repetitive or long running operations, like playing music or pulling in a feed of data from a server.
|
||||
|
||||
3. **Broadcast Receivers**: These are event listeners. The Android operating system uses them to respond to system-wide events.
|
||||
|
||||
4. **Content Providers**: They manage and share app data with other apps installed on the device. For security, data is not generally shared across apps.
|
||||
|
||||
5. **Intents**: These serve as messages or commands to the Android system. They're used to signal to the Android system that certain events have occurred.
|
||||
|
||||
Each app component is designed to serve different purposes and to have a well-defined lifecycle which defines how the component is created and destroyed.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Jetpack Compose
|
||||
|
||||
`Jetpack Compose` is a modern toolkit for building native Android UI. It simplifies and accelerates UI development on Android with less code, powerful tools, and intuitive Kotlin APIs. `Jetpack Compose` offers a declarative approach to designing UI, where you can simply describe what your UI should look like at any given point of your app’s state, and `Compose` takes care of updating the view hierarchy, making UI development more efficient. It also integrates well with existing Android apps, letting you adopt its benefits at your own pace.
|
||||
@@ -0,0 +1,7 @@
|
||||
# App Shortcuts
|
||||
|
||||
Sure, I can provide information about "app shortcuts" feature in Android.
|
||||
|
||||
### App Shortcuts
|
||||
|
||||
App shortcuts in Android are designed to provide quick and convenient routes to specific actions or functions within your app from the device home screen. To use them, long-press an app's icon and a pop-up menu will appear with the available shortcuts. Depending on the app, you might be able to send a message, make a booking, navigate home, or perform some other specific task without having to first open the app and navigate to the desired function. These shortcuts can also be moved and placed individually on the home screen for even faster access.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Navigation Components
|
||||
|
||||
The **Navigation Components** are part of Android Jetpack and are designed to simplify the implementation of navigation in your Android app. These components help you follow best practices, handle deep linking, and provide a consistent user experience across deep and conditional navigation. They also automate many common tasks, such as handling Up and Back actions correctly across many different types of devices. The Navigation component consists of three key parts which are Navigation graph, NavHost, and NavController.
|
||||
@@ -0,0 +1,3 @@
|
||||
# TextView
|
||||
|
||||
`TextView` in Android is a UI (User Interface) element that allows you to display text to the user. You can set the text to be displayed by declaring it in XML or introducing it programmatically. This element supports various attributes such as `android:textAllCaps`, `android:textAppearance`, `android:textColor`, etc., to customize its appearance. Each `TextView` corresponds to an `android.widget.TextView` object. You can also react to user interaction events such as touch or click with the help of listeners (like `View.onClickListener` or `View.onTouchListener`). Manipulating with `TextView` is common when creating Android apps as they form the basic building block for user interface components.
|
||||
@@ -0,0 +1,3 @@
|
||||
# EditText
|
||||
|
||||
`EditText` is a common element used in Android development. It's a fundamental component for accepting user input in simple form or dialog. It allows users to modify text within its bounding box, much like what a TextField does in more general programming languages. If you have used a form or a webpage that allows you to input text, it was probably created with an EditText or similar control. You can customize an `EditText` in various ways including size, color, initial text, and hint text. You can also listen for changes in the text in an EditText, apply filters, specify input types, and more. The `android:text` attribute lets you pre-fill the EditText with text, and `android:hint` provides hint text when the EditText is empty.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Buttons
|
||||
|
||||
Buttons in Android are user interactions that trigger certain programmable actions. They are part of the `View` class in Android, making them an essential part of user interfaces. Android provides different types of buttons such as `Button`, `ToggleButton`, `RadioButton`, `CheckBox`, `Switch`, `ImageButton`, and `FloatingActionButton`. Each of these serves a different purpose and provides varied interactivity options. Their behavior and appearance can be customized in terms of different properties such as text, ID, color, etc. They can be programmed in the XML layout files or dynamically in the Java/Kotlin code.
|
||||
@@ -0,0 +1,3 @@
|
||||
# ImageView
|
||||
|
||||
An `ImageView` is a class used in Android for displaying an image file. It inherits from 'View' class and extends the ability of views to show images. Images can be loaded from various sources such as a resource file, drawable, or a URL, with varying scale types. This class also provides methods to manage the image scale type, define padding, set tint, and manipulate the color filter. It's important to note that `ImageView` should be used in moderation as it is resource-intensive and could degrade app performance if used excessively.
|
||||
@@ -0,0 +1,3 @@
|
||||
# ListView
|
||||
|
||||
`ListView` in Android is a view which groups several items and displays them in vertical scrollable list. The list items are automatically inserted to the list using an `Adapter` that pulls content from a source such as an array or database query and converts each item result into a view that's placed into the list. It is widely used in android apps as it offers an easy way to display a list of data in an organized manner. Functions such as `setAdapter(Adapter)` to specify data source, `setOnItemClickListener(OnItemClickListener)` to listen for click events on items, and `setOnScrollListener(OnScrollListener)` to listen for scroll events, provide further control over the list behavior.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Tabs
|
||||
|
||||
Tabs are commonly used in Android for switching between different views within the same activity. Some instances may make use of a `TabLayout` located within a `ViewPager` to create swipeable tabs. Each tab is usually associated with a fragment. To create tabs in Android, you need to use the `TabLayout` component that is available in the Material Design library. A typical `TabLayout` contains multiple `TabItem`, each representing a tab in the interface. The `TabLayout` works with a `ViewPager` to provide a consistent swipeable interface. Users can approach tabs differently depending on whether they are coded for manual or automatic filling. Manual tab creation and addition require explicit defining of each tab and adding them to the `TabLayout`, while in automatic filling tabs are generated from the `PagerAdaptor`'s page title.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Fragments
|
||||
|
||||
In Android, **Fragments** represent a behavior or a part of the user interface in an Activity. They are modular sections of an activity, which are reusable in different activities. They contribute to making an application adaptive to different devices with varied screen sizes. A fragment has its own lifecycle, receives its own input events, and can be added or removed while the activity is running. While they exist within the context of an activity, they can also be used independently to encapsulate functionality for easier development and reuse. Multiple fragments can combine in a single activity to build a multi-pane UI.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Dialogs
|
||||
|
||||
Dialogs in Android are small windows that prompt users to make a decision or enter additional information. They don't fill the screen and are normally used for modal events that require users to take action before they can proceed. In Android, `Dialog` is actually an abstract class directly subclassed from `Object`. `AlertDialog` is the subclass that you will most commonly use, which contains a number of methods as compared to `Dialog` to support features like lists, checkboxes, radio buttons, and a custom layout design. They are typically used for user interactions such as warnings, notifications, and menus.
|
||||
|
||||
To create a dialog, you must use the `Dialog` class or one of its subclasses, such as `DialogFragment` or `AlertDialog`. For a more detailed explanation on implementing dialogs in Android, you can refer to the official Android Developer's Guide.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Toast
|
||||
|
||||
`Toast` in Android is a simple message that appears on the screen for a short period of time then disappears automatically. It is generally used to provide feedback to the user about an operation in a small popup without requiring any user interaction. This feedback could be an error message or simply information that a process completed successfully. You can configure the toast to appear anywhere on the screen and specify how long it stays up. In order to use a toast, you have to import the `android.widget.Toast` package and instantiate a Toast object. Here's an example of creating a simple toast: `Toast.makeText(context, text, duration).show()`, where context is your application context, text is the message to display, and duration is either `Toast.LENGTH_SHORT` or `Toast.LENGTH_LONG`.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Bottom Sheet
|
||||
|
||||
The **Bottom Sheet** is a popular UI component within the Android development environment. This interface element acts like a drawer that slides up from the bottom of the screen to reveal more content or options. There are two types of bottom sheets in Android: "persistent" and "modal". The persistent bottom sheet shows in-app content that supplements the primary screen content, remaining visible even when the user interacts with the primary surface. On the other hand, the modal bottom sheet is a simple menu presenting a list of options, often used for sharing content, navigating, or for user-editable content. It can be dismissed by the user and does not remain visible when the user interacts with the primary surface.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Drawer
|
||||
|
||||
The `Drawer` in Android is a slide-out menu that enables users to navigate between different parts of an application. It is usually triggered by a hamburger icon in the app's top-left corner. The `Drawer` can contain a list of options, subheadings, and separators to articulate the app's structure. In Android, this component can be implemented using `DrawerLayout` and `NavigationView`. The `DrawerLayout` is the parent component and `NavigationView` is typically placed within the `DrawerLayout`. One key thing to note is that the `Drawer` should not be used as the only means of navigating through an application, according to Android's design guidelines.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Animations
|
||||
|
||||
In Android, animations are used to give a more visual, dynamic, and interactive aspect to a static user interface. Android offers four categories of animation APIs in order to create and manage the animations. These are: **Property Animation**: This allows modification of properties of an object over a given time period. **View Animation**: Consists of two subcategories i.e. Tween Animation (transition of a view from one state to another) and Frame Animation (displaying frames one after another). **Drawable Animation**: This is similar to a slideshow, showing one image after another. Lastly, **Layout Animation**: This is used to animate the layout itself when views are added or removed.
|
||||
|
||||
You can utilize Android's built-in animations or fully customize your own to add unique transitions and movements in your applications. Every app can benefit from a touch of animation to create a more immersive and pleasant user experience.
|
||||
@@ -0,0 +1 @@
|
||||
# Elements
|
||||
@@ -0,0 +1,3 @@
|
||||
# Interface and Navigation
|
||||
|
||||
In Android development, the concepts of "Interface" and "Navigation" are crucial. The "Interface" often refers to the Graphical User Interface (GUI) that users interact with. This includes buttons, text fields, image views, scroll views and other UI elements that the users can interact with to perform certain tasks. Tools like XML and Material Designs are used for interface design in Android. Meanwhile, "Navigation" refers to the interactions that allow users to navigate across, into, and back out from the different pieces of content within the app, following a clear path, like a map. Android uses the Navigation Component, a suite of libraries, tools, and guidance on constructing in-app navigation. Understanding both these elements, Interface and Navigation, is fundamental to creating an intuitive and user-friendly Android application.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Frame
|
||||
|
||||
The `FrameLayout` in Android is a layout manager that pinpoints its children to the top left corner of the layout. This means that all children will be piled up on each other at the same top left corner, which might not be visually appealing. However, it can be useful in some specific designs, where you desire to overlay one view on top of another. Additionally, `FrameLayout` allows you to control the positioning of items in the frame by configuring the `gravity` property. However, bear in mind that this layout does not provide any visual structure – you may need to use `padding` or `margin` to create spaces between the elements in the frame.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user