@@ -35,6 +35,8 @@ import useUndoable from "./useUndoable";
3535import { MultiNodePanel } from "./MultiNodePanel" ;
3636import { getTranslations } from "./translations" ;
3737import { WelcomeMessage } from "./WelcomeMessage" ;
38+ import { ShareDialog } from "./ShareDialog" ;
39+ import { LoadExternalDialog } from "./LoadExternalDialog" ;
3840
3941const nodeTypes = {
4042 topic : TopicNode ,
@@ -51,6 +53,8 @@ export interface LearningMapEditorProps {
5153 roadmapData ?: string | RoadmapData ;
5254 language ?: string ;
5355 onChange ?: ( data : RoadmapData ) => void ;
56+ jsonStore ?: string ;
57+ onLoadExternal ?: ( id : string ) => void ;
5458}
5559
5660const getDefaultFilename = ( ) => {
@@ -63,6 +67,8 @@ export function LearningMapEditor({
6367 roadmapData,
6468 language = "en" ,
6569 onChange,
70+ jsonStore = "https://json.openpatch.org" ,
71+ onLoadExternal,
6672} : LearningMapEditorProps ) {
6773 const { screenToFlowPosition, zoomIn, zoomOut, setCenter, fitView, getNodes, getEdges } = useReactFlow ( ) ;
6874 const [ roadmapState , setRoadmapState , { undo, redo, canUndo, canRedo, reset, resetInitialState } ] = useUndoable < RoadmapData > ( {
@@ -122,6 +128,12 @@ export function LearningMapEditor({
122128 const [ showCompletionOptional , setShowCompletionOptional ] = useState ( true ) ;
123129 const [ showUnlockAfter , setShowUnlockAfter ] = useState ( true ) ;
124130
131+ // Share dialog state
132+ const [ shareDialogOpen , setShareDialogOpen ] = useState ( false ) ;
133+ const [ shareLink , setShareLink ] = useState ( "" ) ;
134+ const [ loadExternalDialogOpen , setLoadExternalDialogOpen ] = useState ( false ) ;
135+ const [ pendingExternalId , setPendingExternalId ] = useState < string | null > ( null ) ;
136+
125137 // Edge drawer state
126138 const [ selectedEdge , setSelectedEdge ] = useState < Edge | null > ( null ) ;
127139 const [ edgeDrawerOpen , setEdgeDrawerOpen ] = useState ( false ) ;
@@ -432,6 +444,66 @@ export function LearningMapEditor({
432444 downloadAnchorNode . remove ( ) ;
433445 } , [ roadmapState ] ) ;
434446
447+ const handleShare = useCallback ( ( ) => {
448+ // Check if map is empty (no nodes)
449+ if ( ! roadmapState . nodes || roadmapState . nodes . length === 0 ) {
450+ alert ( t . emptyMapCannotBeShared ) ;
451+ return ;
452+ }
453+
454+ // Upload to JSON store
455+ fetch ( `${ jsonStore } /api/v2/post` , {
456+ method : "POST" ,
457+ mode : "cors" ,
458+ headers : {
459+ "Content-Type" : "application/json" ,
460+ } ,
461+ body : JSON . stringify ( roadmapState ) ,
462+ } )
463+ . then ( ( r ) => r . json ( ) )
464+ . then ( ( json ) => {
465+ const link = window . location . origin + window . location . pathname + "#json=" + json . id ;
466+ setShareLink ( link ) ;
467+ setShareDialogOpen ( true ) ;
468+ } )
469+ . catch ( ( ) => {
470+ alert ( t . uploadFailed ) ;
471+ } ) ;
472+ } , [ roadmapState , jsonStore , t ] ) ;
473+
474+ const loadFromJsonStore = useCallback ( ( id : string ) => {
475+ fetch ( `${ jsonStore } /api/v2/${ id } ` , {
476+ method : "GET" ,
477+ mode : "cors" ,
478+ } )
479+ . then ( ( r ) => r . text ( ) )
480+ . then ( ( text ) => {
481+ const json = JSON . parse ( text ) ;
482+ setRoadmapState ( json ) ;
483+ loadRoadmapStateIntoReactFlowState ( json ) ;
484+ setLoadExternalDialogOpen ( false ) ;
485+ setPendingExternalId ( null ) ;
486+ } )
487+ . catch ( ( ) => {
488+ alert ( t . loadFailed ) ;
489+ } ) ;
490+ } , [ jsonStore , t , setRoadmapState ] ) ;
491+
492+ const handleLoadExternal = useCallback ( ( id : string ) => {
493+ setPendingExternalId ( id ) ;
494+ setLoadExternalDialogOpen ( true ) ;
495+ } , [ ] ) ;
496+
497+ // Check for external JSON in URL hash on mount
498+ useEffect ( ( ) => {
499+ const hash = window . location . hash ;
500+ if ( hash . startsWith ( "#json=" ) ) {
501+ const id = hash . substring ( 6 ) ;
502+ handleLoadExternal ( id ) ;
503+ }
504+ } , [ handleLoadExternal ] ) ;
505+
506+
435507 const defaultEdgeOptions = {
436508 animated : false ,
437509 style : {
@@ -757,6 +829,7 @@ export function LearningMapEditor({
757829 onOpenSettingsDrawer = { handleOpenSettingsDrawer }
758830 onDownlad = { handleDownload }
759831 onOpen = { handleOpen }
832+ onShare = { handleShare }
760833 language = { effectiveLanguage }
761834 />
762835 { previewMode && < LearningMap roadmapData = { roadmapState } language = { effectiveLanguage } /> }
@@ -863,6 +936,26 @@ export function LearningMapEditor({
863936 </ table >
864937 < button className = "primary-button" onClick = { ( ) => setHelpOpen ( false ) } > { t . close } </ button >
865938 </ dialog >
939+ < ShareDialog
940+ open = { shareDialogOpen }
941+ onClose = { ( ) => setShareDialogOpen ( false ) }
942+ shareLink = { shareLink }
943+ language = { effectiveLanguage }
944+ />
945+ < LoadExternalDialog
946+ open = { loadExternalDialogOpen }
947+ onClose = { ( ) => {
948+ setLoadExternalDialogOpen ( false ) ;
949+ setPendingExternalId ( null ) ;
950+ } }
951+ onDownloadCurrent = { handleDownload }
952+ onReplace = { ( ) => {
953+ if ( pendingExternalId ) {
954+ loadFromJsonStore ( pendingExternalId ) ;
955+ }
956+ } }
957+ language = { effectiveLanguage }
958+ />
866959 </ >
867960 }
868961 </ >
0 commit comments