Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
3 changes: 1 addition & 2 deletions src/bootstrap/agGrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ const productionModules: readonly Module[] = [
PaginationModule,
RowDragModule,
TextEditorModule,
TextFilterModule,
ValidationModule
TextFilterModule
];

export const initializeAgGridModules = () => {
Expand Down
19 changes: 19 additions & 0 deletions src/features/sicp/utils/SicpUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,22 @@ export const readSicpSectionLocalStorage = () => {
const data = readLocalStorage(SICP_CACHE_KEY, SICP_INDEX);
return data;
};

const SICP_SUPPORTED_LANGUAGES = ['en', 'zh_CN'] as const satisfies readonly string[];
export type SicpSupportedLanguage = (typeof SICP_SUPPORTED_LANGUAGES)[number];
export const SICP_DEFAULT_LANGUAGE: SicpSupportedLanguage = 'en';

const sicplanguageKey = 'sicp-textbook-lang';

export const persistSicpLanguageToLocalStorage = (value: string) => {
setLocalStorage(sicplanguageKey, value);
window.dispatchEvent(new Event('sicp-tb-lang-change'));
};

export const getSicpLanguageFromLocalStorage = (): SicpSupportedLanguage | null => {
const value = readLocalStorage(sicplanguageKey, null);
if (!SICP_SUPPORTED_LANGUAGES.includes(value)) {
return null;
}
return value as SicpSupportedLanguage;
};
5 changes: 4 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { initializeAgGridModules } from './bootstrap/agGrid';
import { initializeSentryLogging } from './bootstrap/sentry';
import ApplicationWrapper from './commons/application/ApplicationWrapper';
import { createInBrowserFileSystem } from './pages/fileSystem/createInBrowserFileSystem';
import { SicpLanguageContextProvider } from './pages/sicp/subcomponents/SicpLanguageProvider';

initializeSentryLogging();
initializeAgGridModules();
Expand All @@ -38,7 +39,9 @@ createInBrowserFileSystem(store)
root.render(
<Provider store={store}>
<OverlaysProvider>
<ApplicationWrapper />
<SicpLanguageContextProvider>
<ApplicationWrapper />
</SicpLanguageContextProvider>
</OverlaysProvider>
</Provider>
);
Expand Down
29 changes: 27 additions & 2 deletions src/pages/sicp/Sicp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import SicpErrorBoundary from '../../features/sicp/errors/SicpErrorBoundary';
import getSicpError, { SicpErrorType } from '../../features/sicp/errors/SicpErrors';
import Chatbot from './subcomponents/chatbot/Chatbot';
import SicpIndexPage from './subcomponents/SicpIndexPage';
import { useSicpLanguageContext } from './subcomponents/SicpLanguageProvider';

const baseUrl = Constants.sicpBackendUrl + 'json/';
const extension = '.json';
Expand All @@ -40,6 +41,7 @@ const Sicp: React.FC = () => {
const [loading, setLoading] = useState(false);
const [active, setActive] = useState('0');
const { section } = useParams<{ section: string }>();
const { sicpLanguage, setSicpLanguage } = useSicpLanguageContext();
const parentRef = useRef<HTMLDivElement>(null);
const refs = useRef<Record<string, HTMLElement | null>>({});
const navigate = useNavigate();
Expand Down Expand Up @@ -105,7 +107,7 @@ const Sicp: React.FC = () => {

setLoading(true);

fetch(baseUrl + section + extension)
fetch(`${baseUrl}${sicpLanguage}/${section}${extension}`)
.then(response => {
if (!response.ok) {
throw Error(response.statusText);
Expand All @@ -121,6 +123,7 @@ const Sicp: React.FC = () => {
throw new ParseJsonError(error.message);
}
})

.catch(error => {
console.error(error);

Expand All @@ -138,7 +141,7 @@ const Sicp: React.FC = () => {
.finally(() => {
setLoading(false);
});
}, [section, navigate]);
}, [section, sicpLanguage, navigate]);

// Scroll to correct position
React.useEffect(() => {
Expand All @@ -163,10 +166,31 @@ const Sicp: React.FC = () => {
dispatch(WorkspaceActions.resetWorkspace('sicp'));
dispatch(WorkspaceActions.toggleUsingSubst(false, 'sicp'));
};

const toggleSicpLanguage = () => {
setSicpLanguage(sicpLanguage === 'en' ? 'zh_CN' : 'en');
};

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={toggleSicpLanguage} intent="primary" small>
{sicpLanguage === '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 +210,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
45 changes: 45 additions & 0 deletions src/pages/sicp/subcomponents/SicpLanguageProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { createContext, useCallback, useContext, useState } from 'react';
import {
getSicpLanguageFromLocalStorage,
SICP_DEFAULT_LANGUAGE,
type SicpSupportedLanguage
} from 'src/features/sicp/utils/SicpUtils';

type SicpLanguageContext = {
sicpLanguage: SicpSupportedLanguage;
setSicpLanguage: (lang: SicpSupportedLanguage) => void;
};

const sicpLanguageContext = createContext<SicpLanguageContext | undefined>(undefined);

export const useSicpLanguageContext = (): SicpLanguageContext => {
const context = useContext(sicpLanguageContext);
if (!context) {
throw new Error('useSicpLanguageContext must be used inside an SicpLanguageContextProvider');
}

return context;
};

export const SicpLanguageContextProvider: React.FC<{ children: React.ReactNode }> = ({
children
}) => {
const [lang, setLang] = useState<SicpSupportedLanguage>(
getSicpLanguageFromLocalStorage() ?? SICP_DEFAULT_LANGUAGE
);

const handleLangChange = useCallback((newLang: SicpSupportedLanguage) => {
setLang(newLang);
}, []);

return (
<sicpLanguageContext.Provider
value={{
sicpLanguage: lang,
setSicpLanguage: handleLangChange
}}
>
{children}
</sicpLanguageContext.Provider>
);
};
17 changes: 14 additions & 3 deletions src/pages/sicp/subcomponents/SicpToc.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Tree, TreeNodeInfo } from '@blueprintjs/core';
import { cloneDeep } from 'lodash';
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router';
import Constants from 'src/commons/utils/Constants';

import toc from '../../../features/sicp/data/toc.json';
import fallbackToc from '../../../features/sicp/data/toc.json';
import { useSicpLanguageContext } from './SicpLanguageProvider';

type TocProps = OwnProps;

Expand All @@ -15,8 +17,17 @@ type OwnProps = {
* Table of contents of SICP.
*/
const SicpToc: React.FC<TocProps> = props => {
const [sidebarContent, setSidebarContent] = useState(toc as TreeNodeInfo[]);
const [sidebarContent, setSidebarContent] = useState(fallbackToc as TreeNodeInfo[]);
const navigate = useNavigate();
const { sicpLanguage } = useSicpLanguageContext();

useEffect(() => {
const loadLocalizedToc = async () => {
const resp = await fetch(`${Constants.sicpBackendUrl}json/${sicpLanguage}/toc.json`);
return (await resp.json()) as TreeNodeInfo[];
};
loadLocalizedToc().then(setSidebarContent).catch(console.error);
}, [sicpLanguage]);

const handleNodeExpand = (_node: TreeNodeInfo, path: integer[]) => {
const newState = cloneDeep(sidebarContent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import { MemoryRouter } from 'react-router';
import { renderTreeJson } from 'src/commons/utils/TestUtils';

import SicpIndexPage from '../../subcomponents/SicpIndexPage';
import { SicpLanguageContextProvider } from '../SicpLanguageProvider';

test('Sicp index page', async () => {
const tree = await renderTreeJson(
<MemoryRouter>
<SicpIndexPage />
<SicpLanguageContextProvider>
<SicpIndexPage />
</SicpLanguageContextProvider>
</MemoryRouter>
);
expect(tree).toMatchSnapshot();
Expand Down
5 changes: 4 additions & 1 deletion src/pages/sicp/subcomponents/__tests__/SicpToc.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { MemoryRouter } from 'react-router';
import { renderTreeJson } from 'src/commons/utils/TestUtils';

import { SicpLanguageContextProvider } from '../SicpLanguageProvider';
import SicpToc from '../SicpToc';

test('Sicp toc renders correctly', async () => {
Expand All @@ -10,7 +11,9 @@ test('Sicp toc renders correctly', async () => {

const tree = await renderTreeJson(
<MemoryRouter>
<SicpToc {...props} />
<SicpLanguageContextProvider>
<SicpToc {...props} />
</SicpLanguageContextProvider>
</MemoryRouter>
);
expect(tree).toMatchSnapshot();
Expand Down