diff --git a/src/features/sicp/utils/SicpUtils.ts b/src/features/sicp/utils/SicpUtils.ts index cc347256b1..1e725f0a55 100644 --- a/src/features/sicp/utils/SicpUtils.ts +++ b/src/features/sicp/utils/SicpUtils.ts @@ -11,3 +11,16 @@ export const readSicpSectionLocalStorage = () => { const data = readLocalStorage(SICP_CACHE_KEY, SICP_INDEX); return data; }; + +export const SICP_DEF_TB_LANG = 'en'; +export const SICP_TB_LANG_KEY = 'sicp-textbook-lang'; + +export const setSicpLangLocalStorage = (value: string) => { + setLocalStorage(SICP_TB_LANG_KEY, value); + window.dispatchEvent(new Event('sicp-tb-lang-change')); +}; + +export const readSicpLangLocalStorage = () => { + const data = readLocalStorage(SICP_TB_LANG_KEY, SICP_DEF_TB_LANG); + return data; +}; diff --git a/src/pages/sicp/Sicp.tsx b/src/pages/sicp/Sicp.tsx index 73798b81ca..095519295a 100644 --- a/src/pages/sicp/Sicp.tsx +++ b/src/pages/sicp/Sicp.tsx @@ -2,6 +2,7 @@ import 'katex/dist/katex.min.css'; import { Button, Classes, NonIdealState, Spinner } from '@blueprintjs/core'; import classNames from 'classnames'; +import path from 'path'; import React, { useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useLocation, useNavigate, useParams } from 'react-router'; @@ -14,9 +15,12 @@ import { SicpSection } from 'src/features/sicp/chatCompletion/chatCompletion'; import { parseArr, ParseJsonError } from 'src/features/sicp/parser/ParseJson'; import { getNext, getPrev } from 'src/features/sicp/TableOfContentsHelper'; import { + readSicpLangLocalStorage, readSicpSectionLocalStorage, + setSicpLangLocalStorage, setSicpSectionLocalStorage, SICP_CACHE_KEY, + SICP_DEF_TB_LANG, SICP_INDEX } from 'src/features/sicp/utils/SicpUtils'; @@ -40,7 +44,8 @@ const Sicp: React.FC = () => { const [data, setData] = useState(<>); const [loading, setLoading] = useState(false); const [active, setActive] = useState('0'); - const { section } = useParams<{ section: string }>(); + const { param_lang, section } = useParams<{ param_lang:string, section: string }>(); + const [lang, setLang] = useState(readSicpLangLocalStorage()); const parentRef = useRef(null); const refs = useRef>({}); const navigate = useNavigate(); @@ -89,13 +94,31 @@ const Sicp: React.FC = () => { // Handle loading of latest viewed section and fetch json data React.useEffect(() => { + const valid_langs = ['en', 'zh_CN']; + + if (section && valid_langs.includes(section) || param_lang) { + const plang = param_lang ? param_lang : (section ? section : SICP_DEF_TB_LANG); + if (!valid_langs.includes(plang)) { + setLang(SICP_DEF_TB_LANG); + setSicpLangLocalStorage(SICP_DEF_TB_LANG); + } else { + setLang(plang); + setSicpLangLocalStorage(plang); + } + if (section && valid_langs.includes(section)) { + navigate(`/sicpjs/${SICP_INDEX}`, { replace: true }); + } else { + navigate(`/sicpjs/${section}`, { replace: true }); + } + return; + } if (!section) { /** * Handles rerouting to the latest viewed section when clicking from * the main application navbar. Navigate replace logic is used to allow the * user to still use the browser back button to navigate the app. */ - navigate(`/sicpjs/${readSicpSectionLocalStorage()}`, { replace: true }); + navigate(path.join('sicpjs', readSicpSectionLocalStorage()), { replace: true }); return; } @@ -106,7 +129,11 @@ const Sicp: React.FC = () => { setLoading(true); - fetch(baseUrl + section + extension) + if (!valid_langs.includes(lang)) { + setLang(SICP_DEF_TB_LANG); + setSicpLangLocalStorage(SICP_DEF_TB_LANG); + } + fetch(baseUrl + lang + '/' + section + extension) .then(response => { if (!response.ok) { throw Error(response.statusText); @@ -139,7 +166,7 @@ const Sicp: React.FC = () => { .finally(() => { setLoading(false); }); - }, [section, navigate]); + }, [param_lang, section, lang, navigate]); // Scroll to correct position React.useEffect(() => { @@ -164,10 +191,33 @@ const Sicp: React.FC = () => { dispatch(WorkspaceActions.resetWorkspace('sicp')); dispatch(WorkspaceActions.toggleUsingSubst(false, 'sicp')); }; + + const handleLanguageToggle = () => { + const newLang = lang === 'en' ? 'zh_CN' : 'en'; + setLang(newLang); + setSicpLangLocalStorage(newLang); + }; + const handleNavigation = (sect: string) => { navigate('/sicpjs/' + sect); }; + // Language toggle button with fixed position + const languageToggle = ( +
+ +
+ ); + // `section` is defined due to the navigate logic in the useEffect above const navigationButtons = (
@@ -187,6 +237,7 @@ const Sicp: React.FC = () => { > + {languageToggle} {loading ? (
{loadingComponent}
) : section === 'index' ? ( diff --git a/src/pages/sicp/subcomponents/SicpToc.tsx b/src/pages/sicp/subcomponents/SicpToc.tsx index 84fbced81f..94806d0e03 100644 --- a/src/pages/sicp/subcomponents/SicpToc.tsx +++ b/src/pages/sicp/subcomponents/SicpToc.tsx @@ -1,9 +1,15 @@ import { Tree, TreeNodeInfo } from '@blueprintjs/core'; +import { NonIdealState, Spinner } from '@blueprintjs/core'; import { cloneDeep } from 'lodash'; import React, { useState } from 'react'; import { useNavigate } from 'react-router'; +import Constants from 'src/commons/utils/Constants'; +import { readSicpLangLocalStorage } from 'src/features/sicp/utils/SicpUtils'; -import toc from '../../../features/sicp/data/toc.json'; +import fallbackToc from '../../../features/sicp/data/toc.json'; + +const baseUrl = Constants.sicpBackendUrl + 'json/'; +const loadingComponent = } />; type TocProps = OwnProps; @@ -14,8 +20,9 @@ type OwnProps = { /** * Table of contents of SICP. */ -const SicpToc: React.FC = props => { - const [sidebarContent, setSidebarContent] = useState(toc as TreeNodeInfo[]); + +const Toc: React.FC<{ toc: TreeNodeInfo[], props: TocProps }> = ({toc, props}) => { + const [sidebarContent, setSidebarContent] = useState(toc); const navigate = useNavigate(); const handleNodeExpand = (_node: TreeNodeInfo, path: integer[]) => { @@ -40,15 +47,61 @@ const SicpToc: React.FC = props => { [navigate, props] ); + return ( + + ); +}; + +const SicpToc: React.FC = props => { + const [lang, setLang] = useState(readSicpLangLocalStorage()); + const [toc, setToc] = useState([] as TreeNodeInfo[]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + + React.useEffect(() => { + const handleLangChange = () => { + setLang(readSicpLangLocalStorage()); + } + window.addEventListener('sicp-tb-lang-change', handleLangChange); + return () => window.removeEventListener('sicp-tb-lang-change', handleLangChange) + }, []); + + React.useEffect(() => { + setLoading(true); + fetch(baseUrl + lang + '/toc.json') + .then(response => { + if (!response.ok) { + throw Error(response.statusText); + } + return response.json(); + }) + .then(json => { + setToc(json as TreeNodeInfo[]); + }) + .catch(error => { + console.log(error); + setError(true); + }) + .finally(() => { + setLoading(false); + }); + }, [lang]); + return (
- + {loading ? ( +
{loadingComponent}
+ ) : error ? ( + + ) : ( + + )}
); }; diff --git a/src/routes/routerConfig.tsx b/src/routes/routerConfig.tsx index bcf5d326fe..0441990f9f 100644 --- a/src/routes/routerConfig.tsx +++ b/src/routes/routerConfig.tsx @@ -50,6 +50,7 @@ const commonChildrenRoutes: RouteObject[] = [ { path: 'contributors', lazy: Contributors }, { path: 'callback/github', lazy: GitHubCallback }, { path: 'sicpjs/:section?', lazy: Sicp }, + { path: 'sicpjs/:param_lang/:section?', lazy: Sicp }, { path: 'features', lazy: Features } ];