Skip to content
Draft
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5b990d8
added router params and button to read and change language in url
yihao03 May 14, 2025
f796bda
put textbook lang selection into local storage instead of the url
coder114514 May 20, 2025
da13343
fetch toc based on textbook's lang
coder114514 May 22, 2025
8546da1
update toc react component whenever textbook lang is changed
coder114514 May 29, 2025
32df0bd
put the lang switch at the bottom so it does not cover the toc menu
coder114514 May 29, 2025
ee2eae2
add back the toc.json in the repo as a fallback when there is an erro…
coder114514 May 29, 2025
9ed14ba
SicpToc.tsx: remove unused imports
coder114514 Jun 4, 2025
859f560
Merge branch 'master' into master
martin-henz Jun 10, 2025
f02d7dd
Merge branch 'master' into master
martin-henz Jun 13, 2025
eb61bfd
Merge branch 'master' into master
RichDom2185 Jun 16, 2025
2e63b78
Fix format
RichDom2185 Jun 16, 2025
62bc3dc
Refine According to RD's comments
coder114514 Jun 17, 2025
7356b7b
Resolve 'yarn build' Errors
coder114514 Jun 17, 2025
a1b188a
Merge branch 'master' into master
martin-henz Aug 8, 2025
620e8ff
Merge branch 'master' into master
RichDom2185 Aug 9, 2025
255dc72
Update snapshots
RichDom2185 Aug 9, 2025
0221f4d
Merge branch 'master' of https://github.com/source-academy/frontend i…
RichDom2185 Aug 10, 2025
17c2745
Update conflicting snapshots post-merge
RichDom2185 Aug 10, 2025
276eb7c
Merge branch 'master' of https://github.com/source-academy/frontend i…
RichDom2185 Aug 19, 2025
b9bf2ce
Create SICP language provider
RichDom2185 Aug 19, 2025
daab656
Decouple language logic from UI in SiCP page
RichDom2185 Aug 19, 2025
1b600e9
Remove second param matcher
RichDom2185 Aug 19, 2025
f329360
Use SICP language provider
RichDom2185 Aug 19, 2025
52b4d5d
Revert SICP ToC changes and rewrite logic
RichDom2185 Aug 19, 2025
89fb07c
Merge branch 'master' of https://github.com/source-academy/frontend i…
RichDom2185 Aug 19, 2025
e1e3b66
Remove validation module from production
RichDom2185 Aug 19, 2025
86c2004
Update tests and snapshots
RichDom2185 Aug 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/features/sicp/utils/SicpUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
67 changes: 64 additions & 3 deletions src/pages/sicp/Sicp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,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';

Expand All @@ -35,11 +38,24 @@ export const CodeSnippetContext = React.createContext({

const loadingComponent = <NonIdealState title="Loading Content" icon={<Spinner />} />;

const AVAILABLE_SICP_TB_LANGS: readonly string[] = ['en', 'zh_CN'];

const loadInitialLang = () => {
const saved = readSicpLangLocalStorage();
if (AVAILABLE_SICP_TB_LANGS.includes(saved)) {
return saved;
} else {
setSicpLangLocalStorage(SICP_DEF_TB_LANG);
return SICP_DEF_TB_LANG;
}
};

const Sicp: React.FC = () => {
const [data, setData] = useState(<></>);
const [loading, setLoading] = useState(false);
const [active, setActive] = useState('0');
const { section } = useParams<{ section: string }>();
const { paramLang, section } = useParams<{ paramLang: string; section: string }>();
const [lang, setLang] = useState(loadInitialLang());
const parentRef = useRef<HTMLDivElement>(null);
const refs = useRef<Record<string, HTMLElement | null>>({});
const navigate = useNavigate();
Expand Down Expand Up @@ -88,6 +104,23 @@ const Sicp: React.FC = () => {

// Handle loading of latest viewed section and fetch json data
React.useEffect(() => {
if (paramLang || (section && AVAILABLE_SICP_TB_LANGS.includes(section))) {
const pLang = (paramLang ? paramLang : section)!;
if (AVAILABLE_SICP_TB_LANGS.includes(pLang)) {
setLang(pLang);
setSicpLangLocalStorage(pLang);
} else {
setLang(SICP_DEF_TB_LANG);
setSicpLangLocalStorage(SICP_DEF_TB_LANG);
}
if (paramLang) {
navigate(`/sicpjs/${section}`, { replace: true });
} else {
navigate(`/sicpjs/${readSicpSectionLocalStorage()}`, { replace: true });
}
return;
}

if (!section) {
/**
* Handles rerouting to the latest viewed section when clicking from
Expand All @@ -105,7 +138,11 @@ const Sicp: React.FC = () => {

setLoading(true);

fetch(baseUrl + section + extension)
if (!AVAILABLE_SICP_TB_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);
Expand Down Expand Up @@ -138,7 +175,7 @@ const Sicp: React.FC = () => {
.finally(() => {
setLoading(false);
});
}, [section, navigate]);
}, [paramLang, section, lang, navigate]);

// Scroll to correct position
React.useEffect(() => {
Expand All @@ -163,10 +200,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 = (
<div
style={{
position: 'sticky',
top: '20px',
left: '20px',
zIndex: 0
}}
>
<Button onClick={handleLanguageToggle} intent="primary" small>
{lang === 'en' ? '切换到中文' : 'Switch to English'}
</Button>
</div>
);

// `section` is defined due to the navigate logic in the useEffect above
const navigationButtons = (
<div className="sicp-navigation-buttons">
Expand All @@ -186,6 +246,7 @@ const Sicp: React.FC = () => {
>
<SicpErrorBoundary>
<CodeSnippetContext.Provider value={{ active: active, setActive: handleSnippetEditorOpen }}>
{languageToggle}
{loading ? (
<div className="sicp-content">{loadingComponent}</div>
) : section === 'index' ? (
Expand Down
73 changes: 63 additions & 10 deletions src/pages/sicp/subcomponents/SicpToc.tsx
Original file line number Diff line number Diff line change
@@ -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 = <NonIdealState title="Loading Content" icon={<Spinner />} />;

type TocProps = OwnProps;

Expand All @@ -14,8 +20,9 @@ type OwnProps = {
/**
* Table of contents of SICP.
*/
const SicpToc: React.FC<TocProps> = 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[]) => {
Expand All @@ -40,15 +47,61 @@ const SicpToc: React.FC<TocProps> = props => {
[navigate, props]
);

return (
<Tree
className="sicp-toc-tree"
contents={sidebarContent}
onNodeClick={handleNodeClicked}
onNodeCollapse={handleNodeCollapse}
onNodeExpand={handleNodeExpand}
/>
);
};

const SicpToc: React.FC<TocProps> = 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 (
<div className="sicp-toc">
<Tree
className="sicp-toc-tree"
contents={sidebarContent}
onNodeClick={handleNodeClicked}
onNodeCollapse={handleNodeCollapse}
onNodeExpand={handleNodeExpand}
/>
{loading ? (
<div className="sicp-content">{loadingComponent}</div>
) : error ? (
<Toc toc={fallbackToc as TreeNodeInfo[]} props={props} />
) : (
<Toc toc={toc} props={props} />
)}
</div>
);
};
Expand Down
Loading
Loading