Integrate Readonly Editor

feat/readonly-editor
Arik Chakma 1 year ago
parent f4b92e1073
commit 7acb508c47
  1. 3
      package.json
  2. 19
      pnpm-lock.yaml
  3. 4
      src/components/CustomRoadmap/CustomRoadmap.tsx
  4. 143
      src/components/CustomRoadmap/FlowRoadmapRenderer.tsx
  5. 2
      src/components/CustomRoadmap/ResourceProgressStats.tsx
  6. 14
      src/lib/resource-progress.ts
  7. 2
      tailwind.config.cjs

@ -49,7 +49,8 @@
"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.3"
},
"devDependencies": {
"@playwright/test": "^1.35.1",

@ -89,6 +89,9 @@ dependencies:
tailwindcss:
specifier: ^3.3.3
version: 3.3.3
zustand:
specifier: ^4.4.3
version: 4.4.3(@types/react@18.0.21)(react@18.0.0)
devDependencies:
'@playwright/test':
@ -1078,7 +1081,7 @@ packages:
classcat: 5.0.4
react: 18.0.0
react-dom: 18.0.0(react@18.0.0)
zustand: 4.4.1(@types/react@18.0.21)(react@18.0.0)
zustand: 4.4.3(@types/react@18.0.21)(react@18.0.0)
transitivePeerDependencies:
- '@types/react'
- immer
@ -1094,7 +1097,7 @@ packages:
classcat: 5.0.4
react: 18.0.0
react-dom: 18.0.0(react@18.0.0)
zustand: 4.4.1(@types/react@18.0.21)(react@18.0.0)
zustand: 4.4.3(@types/react@18.0.21)(react@18.0.0)
transitivePeerDependencies:
- '@types/react'
- immer
@ -1116,7 +1119,7 @@ packages:
d3-zoom: 3.0.0
react: 18.0.0
react-dom: 18.0.0(react@18.0.0)
zustand: 4.4.1(@types/react@18.0.21)(react@18.0.0)
zustand: 4.4.3(@types/react@18.0.21)(react@18.0.0)
transitivePeerDependencies:
- '@types/react'
- immer
@ -1136,7 +1139,7 @@ packages:
d3-zoom: 3.0.0
react: 18.0.0
react-dom: 18.0.0(react@18.0.0)
zustand: 4.4.1(@types/react@18.0.21)(react@18.0.0)
zustand: 4.4.3(@types/react@18.0.21)(react@18.0.0)
transitivePeerDependencies:
- '@types/react'
- immer
@ -1154,7 +1157,7 @@ packages:
d3-selection: 3.0.0
react: 18.0.0
react-dom: 18.0.0(react@18.0.0)
zustand: 4.4.1(@types/react@18.0.21)(react@18.0.0)
zustand: 4.4.3(@types/react@18.0.21)(react@18.0.0)
transitivePeerDependencies:
- '@types/react'
- immer
@ -1170,7 +1173,7 @@ packages:
classcat: 5.0.4
react: 18.0.0
react-dom: 18.0.0(react@18.0.0)
zustand: 4.4.1(@types/react@18.0.21)(react@18.0.0)
zustand: 4.4.3(@types/react@18.0.21)(react@18.0.0)
transitivePeerDependencies:
- '@types/react'
- immer
@ -6323,8 +6326,8 @@ packages:
resolution: {integrity: sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==}
dev: false
/zustand@4.4.1(@types/react@18.0.21)(react@18.0.0):
resolution: {integrity: sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==}
/zustand@4.4.3(@types/react@18.0.21)(react@18.0.0):
resolution: {integrity: sha512-oRy+X3ZazZvLfmv6viIaQmtLOMeij1noakIsK/Y47PWYhT8glfXzQ4j0YcP5i0P0qI1A4rIB//SGROGyZhx91A==}
engines: {node: '>=12.7.0'}
peerDependencies:
'@types/react': '>=16.8'

@ -14,6 +14,7 @@ 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,7 +122,8 @@ export function CustomRoadmap() {
return (
<>
<RoadmapHeader />
<RoadmapRenderer roadmap={roadmap!} />
{/* <RoadmapRenderer roadmap={roadmap!} /> */}
<FlowRoadmapRenderer roadmap={roadmap!} />
<TopicDetail canSubmitContribution={false} />
<UserProgressModal
resourceId={roadmap?._id!}

@ -0,0 +1,143 @@
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 } from 'react';
import { calculateDimensions } from '../../../editor/utils/roadmap';
type FlowRoadmapRendererProps = {
roadmap: RoadmapDocument;
};
export function FlowRoadmapRenderer(props: FlowRoadmapRendererProps) {
const { roadmap } = props;
const roadmapId = String(roadmap._id!);
const toast = useToast();
const { measuredHeight } = useMemo(
() => calculateDimensions(roadmap?.nodes || [], 100),
[roadmap?.nodes]
);
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;
}
console.log(target);
const isCurrentStatusDone = target?.classList.contains('done');
console.log(target?.classList, isCurrentStatusDone);
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 (
<ReadonlyEditor
roadmap={roadmap}
style={{
height: measuredHeight,
}}
onRendered={(wrapperRef) => {
renderResourceProgress('roadmap', roadmapId).then(() => {});
}}
onTopicClick={handleTopicClick}
onTopicRightClick={handleTopicRightClick}
onTopicShiftClick={handleTopicShiftClick}
onTopicAltClick={handleTopicAltClick}
onButtonNodeClick={handleLinkClick}
onLinkClick={handleLinkClick}
fontFamily="Balsamiq Sans"
fontURL="/fonts/balsamiq.woff2"
/>
);
}

@ -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,

@ -213,6 +213,7 @@ export function topicSelectorAll(
`[data-group-id="${topicId}"]`, // Elements with exact match of the topic id
`[data-group-id="check:${topicId}"]`, // Matching "check:XXXX" box of the topic
`[data-node-id="${topicId}"]`, // Matching custom roadmap nodes
`[data-id="${topicId}"]`, // Matching custom roadmap nodes
],
parentElement
).forEach((element) => {
@ -257,6 +258,8 @@ export function clearResourceProgress() {
'.clickable-group',
'[data-type="topic"]',
'[data-type="subtopic"]',
'.react-flow__node-topic',
'.react-flow__node-subtopic',
]);
for (const clickableElement of matchingElements) {
clickableElement.classList.remove('done', 'skipped', 'learning', 'removed');
@ -314,6 +317,8 @@ export function refreshProgressCounters() {
'.clickable-group',
'[data-type="topic"]',
'[data-type="subtopic"]',
'.react-flow__node-topic',
'.react-flow__node-subtopic',
]).length;
const externalLinks = document.querySelectorAll(
@ -350,15 +355,20 @@ export function refreshProgressCounters() {
getMatchingElements([
'.clickable-group.done:not([data-group-id^="ext_link:"])',
'[data-node-id].done', // All data-node-id=*.done elements are custom roadmap nodes
'[data-id].done', // All data-id=*.done elements are custom roadmap nodes
]).length - totalCheckBoxesDone;
const totalLearning =
getMatchingElements([
'.clickable-group.learning',
'[data-node-id].learning',
'[data-id].learning',
]).length - totalCheckBoxesLearning;
const totalSkipped =
getMatchingElements(['.clickable-group.skipped', '[data-node-id].skipped'])
.length - totalCheckBoxesSkipped;
getMatchingElements([
'.clickable-group.skipped',
'[data-node-id].skipped',
'[data-id].skipped',
]).length - totalCheckBoxesSkipped;
const doneCountEls = document.querySelectorAll('[data-progress-done]');
if (doneCountEls.length > 0) {

@ -1,6 +1,6 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue,svg}'],
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue,svg}', './editor/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue,svg}'],
future: {
hoverOnlyWhenSupported: true,
},

Loading…
Cancel
Save