From 26eafdc341867fb4f5ac8634c0abab67f2688696 Mon Sep 17 00:00:00 2001
From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com>
Date: Fri, 18 Apr 2025 13:53:23 +0800
Subject: [PATCH 1/4] fix(locales): invalid interpolation patterns
---
client/locales/zh.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/client/locales/zh.json b/client/locales/zh.json
index df0f47c1a4d..80a5747cf71 100644
--- a/client/locales/zh.json
+++ b/client/locales/zh.json
@@ -3312,7 +3312,7 @@
"defaultMessage": "运行代码"
},
"course.assessment.submission.runCodeWithLimit": {
- "defaultMessage": "运行代码({attempts Left, pleural, one {# attempt} other {# attempts}} left)"
+ "defaultMessage": "运行代码({attemptsLeft, plural, one {# attempt} other {# attempts}} left)"
},
"course.assessment.submission.saveDraft": {
"defaultMessage": "保存草稿"
@@ -4725,7 +4725,7 @@
"defaultMessage": "无法创建 {numFailed} 个组。"
},
"course.group.GroupShow.GroupManager.GroupManager.createMultiplePartialFailure": {
- "defaultMessage": "无法创建 {numFailed} {numFailed,复数,一个 {group} 其他 {groups}}。"
+ "defaultMessage": "无法创建 {numFailed} {numFailed, plural, one {group} other {groups}}。"
},
"course.group.GroupShow.GroupManager.GroupManager.createMultipleSuccess": {
"defaultMessage": "{numCreated} {numCreated, plural, one {group was} other {groups were}} 已成功创建。"
From 833273ce08b11e71f5c779b8aee1e1c31b5eb736 Mon Sep 17 00:00:00 2001
From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com>
Date: Fri, 18 Apr 2025 13:53:56 +0800
Subject: [PATCH 2/4] fix(AchievementReordering): clarify button label
---
.../achievement/components/misc/AchievementReordering.tsx | 2 +-
client/locales/en.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx b/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx
index e0011aa1349..e1bdb3dee55 100644
--- a/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx
+++ b/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx
@@ -26,7 +26,7 @@ const translations = defineMessages({
},
endReorderAchievement: {
id: 'course.achievement.AchievementReordering.endReorderAchievement',
- defaultMessage: 'Save New Ordering',
+ defaultMessage: 'Done reordering',
},
updateFailed: {
id: 'course.achievement.AchievementReordering.updateFailed',
diff --git a/client/locales/en.json b/client/locales/en.json
index deac903fe32..8bc2de49963 100644
--- a/client/locales/en.json
+++ b/client/locales/en.json
@@ -306,7 +306,7 @@
"defaultMessage": "New Achievement"
},
"course.achievement.AchievementReordering.endReorderAchievement": {
- "defaultMessage": "Save New Ordering"
+ "defaultMessage": "Done reordering"
},
"course.achievement.AchievementReordering.startReorderAchievement": {
"defaultMessage": "Reorder"
From d6aef7e6450d99b560c8b7d73f0006050fe45e28 Mon Sep 17 00:00:00 2001
From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com>
Date: Fri, 18 Apr 2025 13:54:38 +0800
Subject: [PATCH 3/4] perf(jquery): lazy load jquery and ace language modes
- remove jQuery check from `wait_for_ajax` rspec helper
- lazy load jQuery, jQuery UI for achievement reordering page
- lazy load non-python language modes and removed jquery initializer
---
client/app/__test__/setup.js | 4 -
.../components/misc/AchievementReordering.tsx | 97 ++++++++-----
.../LiveFeedbackHistory/LiveFeedbackFiles.tsx | 5 +-
.../components/answers/Programming/index.jsx | 3 -
client/app/initializers.js | 1 -
.../components/core/fields/EditorField.tsx | 52 ++++++-
.../lib/components/wrappers/ThemeProvider.tsx | 2 +-
client/app/lib/initializers/ace-editor.js | 129 ------------------
client/webpack.common.js | 7 -
spec/support/capybara.rb | 2 +-
10 files changed, 116 insertions(+), 186 deletions(-)
delete mode 100644 client/app/lib/initializers/ace-editor.js
diff --git a/client/app/__test__/setup.js b/client/app/__test__/setup.js
index f34b92f8144..0fcb817bfdd 100644
--- a/client/app/__test__/setup.js
+++ b/client/app/__test__/setup.js
@@ -15,8 +15,6 @@ import './mocks/matchMedia';
Enzyme.configure({ adapter: new Adapter() });
require('@babel/polyfill');
-// Our jquery is from CDN and loaded at runtime, so this is required in test.
-const jQuery = require('jquery');
const timeZone = 'Asia/Singapore';
const intlCache = createIntlCache();
@@ -47,8 +45,6 @@ const buildContextOptions = (store) => {
global.courseId = courseId;
global.window = window;
global.muiTheme = muiTheme;
-global.$ = jQuery;
-global.jQuery = jQuery;
global.buildContextOptions = buildContextOptions;
window.history.pushState({}, '', `/courses/${courseId}`);
diff --git a/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx b/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx
index e1bdb3dee55..89288695de2 100644
--- a/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx
+++ b/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx
@@ -1,24 +1,16 @@
+import { useRef, useState } from 'react';
import { defineMessages } from 'react-intl';
-import { Button } from '@mui/material';
+import { LoadingButton } from '@mui/lab';
import CourseAPI from 'api/course';
import toast from 'lib/hooks/toast';
import useTranslation from 'lib/hooks/useTranslation';
-require('jquery-ui/ui/widgets/sortable');
-
interface AchievementReorderingProps {
handleReordering: (state: boolean) => void;
isReordering: boolean;
}
-const styles = {
- AchievementReorderingButton: {
- fontSize: 14,
- marginRight: 12,
- },
-};
-
const translations = defineMessages({
startReorderAchievement: {
id: 'course.achievement.AchievementReordering.startReorderAchievement',
@@ -38,12 +30,6 @@ const translations = defineMessages({
},
});
-// Serialise the ordered achievements as data for the API call.
-function serializedOrdering(): string {
- const options = { attribute: 'achievementid', key: 'achievement_order[]' };
- return $('tbody').first().sortable('serialize', options);
-}
-
const AchievementReordering = (
props: AchievementReorderingProps,
): JSX.Element => {
@@ -60,35 +46,80 @@ const AchievementReordering = (
}
}
+ const [loadingSortable, setLoadingSortable] = useState(false);
+
+ const sortableCallbacksRef = useRef<{
+ enable: () => void;
+ disable: () => void;
+ }>();
+
return (
-
+
);
};
diff --git a/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackFiles.tsx b/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackFiles.tsx
index 192f1e1c003..f605762135f 100644
--- a/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackFiles.tsx
+++ b/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackFiles.tsx
@@ -1,5 +1,4 @@
-import { FC, useRef, useState } from 'react';
-import ReactAce from 'react-ace';
+import { ComponentRef, FC, useRef, useState } from 'react';
import { MessageFile } from 'types/course/assessment/submission/liveFeedback';
import ProgrammingFileDownloadChip from 'course/assessment/submission/components/answers/Programming/ProgrammingFileDownloadChip';
@@ -12,7 +11,7 @@ interface Props {
const LiveFeedbackFiles: FC = (props) => {
const { file } = props;
- const editorRef = useRef(null);
+ const editorRef = useRef>(null);
const [selectedLine, setSelectedLine] = useState(1);
diff --git a/client/app/bundles/course/assessment/submission/components/answers/Programming/index.jsx b/client/app/bundles/course/assessment/submission/components/answers/Programming/index.jsx
index 681af3e71ba..27d3d871e3b 100644
--- a/client/app/bundles/course/assessment/submission/components/answers/Programming/index.jsx
+++ b/client/app/bundles/course/assessment/submission/components/answers/Programming/index.jsx
@@ -7,9 +7,6 @@ import { getIsSavingAnswer } from 'course/assessment/submission/selectors/answer
import { getSubmission } from 'course/assessment/submission/selectors/submissions';
import { useAppSelector } from 'lib/hooks/store';
-import 'ace-builds/src-noconflict/mode-python';
-import 'ace-builds/src-noconflict/theme-github';
-
import CodaveriFeedbackStatus from '../../../containers/CodaveriFeedbackStatus';
import ProgrammingImportEditor from '../../../containers/ProgrammingImport/ProgrammingImportEditor';
import { questionShape } from '../../../propTypes';
diff --git a/client/app/initializers.js b/client/app/initializers.js
index eb9d60a4816..264d98d39d0 100644
--- a/client/app/initializers.js
+++ b/client/app/initializers.js
@@ -1,7 +1,6 @@
/* eslint-disable global-require */
function loadModules() {
- require('lib/initializers/ace-editor');
// Require web font last so that it doesn't block the load of current module.
require('lib/initializers/webfont');
}
diff --git a/client/app/lib/components/core/fields/EditorField.tsx b/client/app/lib/components/core/fields/EditorField.tsx
index 953023f138c..39c7fd3809b 100644
--- a/client/app/lib/components/core/fields/EditorField.tsx
+++ b/client/app/lib/components/core/fields/EditorField.tsx
@@ -1,8 +1,15 @@
-import { ComponentProps, ForwardedRef, forwardRef } from 'react';
+import {
+ ComponentProps,
+ ForwardedRef,
+ forwardRef,
+ useEffect,
+ useState,
+} from 'react';
import AceEditor from 'react-ace';
import { LanguageMode } from 'types/course/assessment/question/programming';
import 'ace-builds/src-noconflict/theme-github';
+import 'ace-builds/src-noconflict/mode-python';
import './AceEditor.css';
@@ -30,11 +37,48 @@ const DEFAULT_FONT_FAMILY = [
'monospace',
].join(',');
+/**
+ * Loads Ace's mode scripts on demand for `language` and returns `true` if the mode
+ * has been loaded.
+ *
+ * Remember to update the regex in the `webpackInclude` comment below to bundle any
+ * new languages we support in the future.
+ */
+const useLazyMode = (language: LanguageMode): boolean => {
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ setLoading(true);
+
+ let ignore = false;
+
+ (async (): Promise => {
+ await import(
+ /* webpackInclude: /ace-builds\/src-noconflict\/mode-(c_cpp|python|r|java|javascript)\./ */
+ /* webpackChunkName: "ace-[request]" */
+ `ace-builds/src-noconflict/mode-${language}`
+ );
+
+ if (ignore) return;
+
+ setLoading(false);
+ })();
+
+ return () => {
+ ignore = true;
+ };
+ }, [language]);
+
+ return loading;
+};
+
const EditorField = forwardRef(
(props: EditorProps, ref: ForwardedRef): JSX.Element => {
const { language, value, disabled, onChange, cursorStart, ...otherProps } =
props;
+ const loading = useLazyMode(language);
+
return (
{
classes: {
root: 'rounded-full',
sizeLarge: 'px-5 py-3',
- sizeMedium: 'px-3 py-2',
+ sizeMedium: 'px-2 py-2',
sizeSmall: 'min-w-[6rem] px-1 py-1',
},
},
diff --git a/client/app/lib/initializers/ace-editor.js b/client/app/lib/initializers/ace-editor.js
deleted file mode 100644
index 38d69c52d0c..00000000000
--- a/client/app/lib/initializers/ace-editor.js
+++ /dev/null
@@ -1,129 +0,0 @@
-const ace = require('ace-builds');
-
-// Load ACE dependencies required for ACE syntax highlighting and theming
-require('ace-builds/src-noconflict/mode-c_cpp');
-require('ace-builds/src-noconflict/mode-java');
-require('ace-builds/src-noconflict/mode-javascript');
-require('ace-builds/src-noconflict/mode-python');
-require('ace-builds/src-noconflict/mode-r');
-require('ace-builds/src-noconflict/theme-github');
-
-/**
- * Builds a new Ace editor container, with the given ID as a suffix.
- *
- * @param {String} id The ID to use as a suffix for the Ace editors.
- * @returns {jQuery}
- */
-function buildEditorContainer(id) {
- const $editor = $('');
- $editor.css({
- display: 'none',
- position: 'relative',
- });
- $editor[0].id = `ace_${id}`;
-
- return $editor;
-}
-
-/**
- * Builds the editor within the given container.
- *
- * @param {jQuery} $container The container to build the editor in.
- * @param {Object} options The options for the editor.
- * @returns {Object} The Ace editor instance.
- */
-function buildEditor($container, options) {
- const editor = ace.edit($container[0]);
- editor.setTheme(`ace/theme/${options.theme}`);
- editor.getSession().setMode(`ace/mode/${options.lang}`);
-
- editor.setOptions({ readOnly: !!options.readOnly });
-
- return editor;
-}
-
-/**
- * Builds the Ace configuration object.
- *
- * @param {jQuery} $container The container for the Ace editor.
- * @param {Object} aceEditor The Ace editor created.
- * @returns {Object}
- */
-function buildAceOptions($container, aceEditor) {
- return {
- container: $container,
- editor: aceEditor,
- };
-}
-
-/**
- * Assigns the Ace configuration to the given element.
- *
- * @param {jQuery} $element The element which has the Ace editor associated with.
- * @param {jQuery} $container The container element for the Ace editor.
- * @param {Object} aceEditor The Ace editor created from `ace.edit`.
- */
-function assignEditor($element, $container, aceEditor) {
- $element.data('ace', buildAceOptions($container, aceEditor));
-}
-
-/**
- * Finds or builds an editor container from the given jQuery element.
- *
- * @param {jQuery} $element The element to find or build an Ace editor for.
- * @param {Object} options The options to use when building the element.
- * @returns {jQuery}
- */
-function findOrBuildEditorContainer($element, options) {
- const aceData = $element.data('ace');
- if (aceData) {
- return aceData.container;
- }
-
- const $container = buildEditorContainer($element[0].id);
- $container.insertAfter($element);
- $container.height($element.height());
-
- const editor = buildEditor($container, options);
- editor.on('change', () => {
- $element.val(editor.session.getValue());
- });
- editor.session.setValue($element.val());
- assignEditor($element, $container, editor);
-
- return $container;
-}
-
-/**
- * Redirects all labels from the old textarea to point to the new editor.
- * @param {jQuery} $from The textarea to redirect the labels from.
- * @param {jQuery} $editor The container for the Ace editor to redirect the labels to.
- */
-function reassignLabelsToAce($from, $editor) {
- const fromId = $from[0].id;
- const $editorTextarea = $('textarea.ace_text-input', $editor);
- $editorTextarea[0].id = `ace_textarea_${fromId}`;
- $(`label[for="${fromId}"]`).attr('for', $editorTextarea[0].id);
-}
-
-$.fn.ace = function (opt) {
- const options = $.extend({}, $.fn.ace.defaults, opt);
- return this.each(function () {
- const $this = $(this);
- const elementOptions = $.extend({}, options, {
- lang: this.lang,
- readOnly: $this[0].readOnly,
- });
- const $editor = findOrBuildEditorContainer($this, elementOptions);
-
- $this.hide();
- $editor.show();
-
- reassignLabelsToAce($this, $editor);
- });
-};
-
-$.fn.ace.defaults = {
- theme: 'github',
- lang: null,
-};
diff --git a/client/webpack.common.js b/client/webpack.common.js
index fcc8623d3d0..5b96da75f32 100644
--- a/client/webpack.common.js
+++ b/client/webpack.common.js
@@ -164,13 +164,6 @@ module.exports = {
],
exclude: /node_modules/,
},
- {
- test: require.resolve('jquery'),
- loader: 'expose-loader',
- options: {
- exposes: ['jQuery', '$'],
- },
- },
{
test: require.resolve('./app/lib/moment-timezone'),
loader: 'expose-loader',
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 86c83b520d3..14832805b1c 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -31,7 +31,7 @@ def find_form(selector, action: nil)
def wait_for_ajax
Timeout.timeout(Capybara.default_max_wait_time) do
- sleep 0.1 until page.evaluate_script('jQuery.active').zero?
+ sleep 0.1
end
end
From e6a6423bf8ce95e79bee8ceb76edf2538f26e001 Mon Sep 17 00:00:00 2001
From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com>
Date: Fri, 18 Apr 2025 13:57:58 +0800
Subject: [PATCH 4/4] fix(moment): import moment from lib/moment only, trim
timezone data to >=2014
---
client/app/bundles/common/DashboardPage.tsx | 2 +-
.../components/HeartbeatsTimeline.tsx | 2 +-
.../components/HeartbeatsTimelineChart.tsx | 2 +-
.../assessment/pages/AssessmentMonitoring/utils.ts | 2 +-
.../LiveFeedbackHistory/LiveFeedbackMessageHistory.tsx | 3 +--
.../submission/reducers/liveFeedbackChats/index.ts | 3 +--
.../components/DayCalendar/DayCalendar.tsx | 2 +-
.../components/DayCalendar/DayColumn.tsx | 3 ++-
.../reference-timelines/components/SubmitIndicator.tsx | 2 +-
.../components/TimeBar/DurationBar.tsx | 3 ++-
.../reference-timelines/components/TimeBar/TimeBar.tsx | 3 ++-
.../components/TimeBar/TimeBarHandle.tsx | 3 ++-
.../components/TimePopup/TimePopup.tsx | 2 +-
.../components/TimePopup/TimePopupForm.tsx | 2 +-
.../components/TimelinesStack/AssignableTimeline.tsx | 3 ++-
.../components/TimelinesStack/Timeline.tsx | 2 +-
.../reference-timelines/contexts/LastSavedContext.tsx | 3 ++-
client/app/bundles/course/reference-timelines/utils.ts | 3 ++-
.../reference-timelines/views/DayView/ItemsSidebar.tsx | 3 ++-
client/package.json | 1 +
client/webpack.prod.js | 2 ++
client/yarn.lock | 10 +++++++++-
22 files changed, 39 insertions(+), 22 deletions(-)
diff --git a/client/app/bundles/common/DashboardPage.tsx b/client/app/bundles/common/DashboardPage.tsx
index 4798315b944..be171e86977 100644
--- a/client/app/bundles/common/DashboardPage.tsx
+++ b/client/app/bundles/common/DashboardPage.tsx
@@ -2,7 +2,6 @@ import { defineMessages } from 'react-intl';
import { Navigate } from 'react-router-dom';
import { ArrowForward } from '@mui/icons-material';
import { Avatar, Stack, Typography } from '@mui/material';
-import moment from 'moment';
import { HomeLayoutCourseData } from 'types/home';
import { getCourseLogoUrl } from 'course/helper';
@@ -13,6 +12,7 @@ import { useAppContext } from 'lib/containers/AppContainer';
import { getUrlParameter } from 'lib/helpers/url-helpers';
import useItems from 'lib/hooks/items/useItems';
import useTranslation from 'lib/hooks/useTranslation';
+import moment from 'lib/moment';
import NewCourseButton from './components/NewCourseButton';
diff --git a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimeline.tsx b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimeline.tsx
index ea2939f22d5..e14221272c6 100644
--- a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimeline.tsx
+++ b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimeline.tsx
@@ -1,8 +1,8 @@
import { useState } from 'react';
-import moment from 'moment';
import { HeartbeatDetail } from 'types/channels/liveMonitoring';
import { BrowserAuthorizationMethod } from 'course/assessment/components/monitoring/BrowserAuthorizationMethodOptionsFormFields/common';
+import moment from 'lib/moment';
import HeartbeatDetailCard from './HeartbeatDetailCard';
import HeartbeatsTimelineChart from './HeartbeatsTimelineChart';
diff --git a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimelineChart.tsx b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimelineChart.tsx
index 3e47f456841..68af626e572 100644
--- a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimelineChart.tsx
+++ b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimelineChart.tsx
@@ -10,12 +10,12 @@ import {
PointStyle,
} from 'chart.js';
import zoomPlugin from 'chartjs-plugin-zoom';
-import moment from 'moment';
import palette from 'theme/palette';
import { HeartbeatDetail } from 'types/channels/liveMonitoring';
import { useAppSelector } from 'lib/hooks/store';
import useTranslation from 'lib/hooks/useTranslation';
+import moment from 'lib/moment';
import translations from '../../../translations';
import { select } from '../selectors';
diff --git a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/utils.ts b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/utils.ts
index 3ab5a503a8a..ca23d6dc429 100644
--- a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/utils.ts
+++ b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/utils.ts
@@ -1,4 +1,4 @@
-import moment from 'moment';
+import moment from 'lib/moment';
export type Presence = 'alive' | 'late' | 'missing';
diff --git a/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackMessageHistory.tsx b/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackMessageHistory.tsx
index 7860f3ca075..fa9f6f7b250 100644
--- a/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackMessageHistory.tsx
+++ b/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackMessageHistory.tsx
@@ -1,6 +1,5 @@
import { Dispatch, FC, SetStateAction } from 'react';
import { Typography } from '@mui/material';
-import moment from 'moment';
import { LiveFeedbackChatMessage } from 'types/course/assessment/submission/liveFeedback';
import {
@@ -8,7 +7,7 @@ import {
justifyPosition,
} from 'course/assessment/submission/components/GetHelpChatPage/utils';
import MarkdownText from 'course/assessment/submission/components/MarkdownText';
-import { SHORT_DATE_TIME_FORMAT } from 'lib/moment';
+import moment, { SHORT_DATE_TIME_FORMAT } from 'lib/moment';
interface Props {
messages: LiveFeedbackChatMessage[];
diff --git a/client/app/bundles/course/assessment/submission/reducers/liveFeedbackChats/index.ts b/client/app/bundles/course/assessment/submission/reducers/liveFeedbackChats/index.ts
index 60f12b53400..b5e115df408 100644
--- a/client/app/bundles/course/assessment/submission/reducers/liveFeedbackChats/index.ts
+++ b/client/app/bundles/course/assessment/submission/reducers/liveFeedbackChats/index.ts
@@ -5,9 +5,8 @@ import {
PayloadAction,
} from '@reduxjs/toolkit';
import { shuffle } from 'lodash';
-import moment from 'moment';
-import { SHORT_TIME_FORMAT } from 'lib/moment';
+import moment, { SHORT_TIME_FORMAT } from 'lib/moment';
import {
getLocalStorageValue,
diff --git a/client/app/bundles/course/reference-timelines/components/DayCalendar/DayCalendar.tsx b/client/app/bundles/course/reference-timelines/components/DayCalendar/DayCalendar.tsx
index 719499bdd2c..d3ba32d4527 100644
--- a/client/app/bundles/course/reference-timelines/components/DayCalendar/DayCalendar.tsx
+++ b/client/app/bundles/course/reference-timelines/components/DayCalendar/DayCalendar.tsx
@@ -2,9 +2,9 @@ import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { FixedSizeList as List } from 'react-window';
import { Button, Typography } from '@mui/material';
-import moment from 'moment';
import useTranslation from 'lib/hooks/useTranslation';
+import moment from 'lib/moment';
import translations from '../../translations';
import {
diff --git a/client/app/bundles/course/reference-timelines/components/DayCalendar/DayColumn.tsx b/client/app/bundles/course/reference-timelines/components/DayCalendar/DayColumn.tsx
index 4ca273feeb8..7cee1062211 100644
--- a/client/app/bundles/course/reference-timelines/components/DayCalendar/DayColumn.tsx
+++ b/client/app/bundles/course/reference-timelines/components/DayCalendar/DayColumn.tsx
@@ -1,7 +1,8 @@
import { CSSProperties, memo } from 'react';
import { areEqual } from 'react-window';
import { Typography } from '@mui/material';
-import moment from 'moment';
+
+import moment from 'lib/moment';
import { getSecondsFromDays, isToday, isWeekend } from '../../utils';
diff --git a/client/app/bundles/course/reference-timelines/components/SubmitIndicator.tsx b/client/app/bundles/course/reference-timelines/components/SubmitIndicator.tsx
index 416fd39637f..8d77d7dca2b 100644
--- a/client/app/bundles/course/reference-timelines/components/SubmitIndicator.tsx
+++ b/client/app/bundles/course/reference-timelines/components/SubmitIndicator.tsx
@@ -1,10 +1,10 @@
import { useEffect, useState } from 'react';
import { Cancel, CheckCircle } from '@mui/icons-material';
import { Chip, Grow, Tooltip, Typography } from '@mui/material';
-import moment from 'moment';
import LoadingIndicator from 'lib/components/core/LoadingIndicator';
import useTranslation from 'lib/hooks/useTranslation';
+import moment from 'lib/moment';
import { useLastSaved } from '../contexts';
import translations from '../translations';
diff --git a/client/app/bundles/course/reference-timelines/components/TimeBar/DurationBar.tsx b/client/app/bundles/course/reference-timelines/components/TimeBar/DurationBar.tsx
index b5b64dc5b89..82d74dd9123 100644
--- a/client/app/bundles/course/reference-timelines/components/TimeBar/DurationBar.tsx
+++ b/client/app/bundles/course/reference-timelines/components/TimeBar/DurationBar.tsx
@@ -1,5 +1,6 @@
import { MouseEventHandler, ReactNode, TouchEventHandler } from 'react';
-import moment from 'moment';
+
+import moment from 'lib/moment';
import {
DAY_WIDTH_PIXELS,
diff --git a/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBar.tsx b/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBar.tsx
index f2d206374ed..9dbae1ef05a 100644
--- a/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBar.tsx
+++ b/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBar.tsx
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
-import moment from 'moment';
+
+import moment from 'lib/moment';
import { DAY_WIDTH_PIXELS, getDaysFromWidth } from '../../utils';
import HorizontallyDraggable from '../HorizontallyDraggable';
diff --git a/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBarHandle.tsx b/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBarHandle.tsx
index 9c5c2a28c03..b6cd8a2ac82 100644
--- a/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBarHandle.tsx
+++ b/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBarHandle.tsx
@@ -5,7 +5,8 @@ import {
TouchEventHandler,
} from 'react';
import { Typography } from '@mui/material';
-import moment from 'moment';
+
+import moment from 'lib/moment';
interface HandleContentProps {
side: 'start' | 'end';
diff --git a/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopup.tsx b/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopup.tsx
index ceb9814270b..209aca0c70b 100644
--- a/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopup.tsx
+++ b/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopup.tsx
@@ -1,5 +1,4 @@
import { Typography } from '@mui/material';
-import moment from 'moment';
import {
ItemWithTimeData,
TimelineData,
@@ -8,6 +7,7 @@ import {
import { useAppDispatch } from 'lib/hooks/store';
import toast from 'lib/hooks/toast';
import useTranslation from 'lib/hooks/useTranslation';
+import moment from 'lib/moment';
import { useSetLastSaved } from '../../contexts';
import { createTime, deleteTime, updateTime } from '../../operations';
diff --git a/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopupForm.tsx b/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopupForm.tsx
index b7a7309ffa8..bdb650773d4 100644
--- a/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopupForm.tsx
+++ b/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopupForm.tsx
@@ -1,11 +1,11 @@
import { Controller } from 'react-hook-form';
import { Button, Collapse } from '@mui/material';
-import moment from 'moment';
import { date, object, ref } from 'yup';
import FormDateTimePickerField from 'lib/components/form/fields/DateTimePickerField';
import Form from 'lib/components/form/Form';
import useTranslation from 'lib/hooks/useTranslation';
+import moment from 'lib/moment';
import formTranslations from 'lib/translations/form';
import { useLastSaved } from '../../contexts';
diff --git a/client/app/bundles/course/reference-timelines/components/TimelinesStack/AssignableTimeline.tsx b/client/app/bundles/course/reference-timelines/components/TimelinesStack/AssignableTimeline.tsx
index 989b619bf31..1256c169631 100644
--- a/client/app/bundles/course/reference-timelines/components/TimelinesStack/AssignableTimeline.tsx
+++ b/client/app/bundles/course/reference-timelines/components/TimelinesStack/AssignableTimeline.tsx
@@ -1,11 +1,12 @@
import { useState } from 'react';
-import moment from 'moment';
import {
ItemWithTimeData,
TimeData,
TimelineData,
} from 'types/course/referenceTimelines';
+import moment from 'lib/moment';
+
import { useLastSaved } from '../../contexts';
import { DraftableTimeData } from '../../utils';
import TimePopup from '../TimePopup';
diff --git a/client/app/bundles/course/reference-timelines/components/TimelinesStack/Timeline.tsx b/client/app/bundles/course/reference-timelines/components/TimelinesStack/Timeline.tsx
index c24c262ccc8..9cb7223ab5b 100644
--- a/client/app/bundles/course/reference-timelines/components/TimelinesStack/Timeline.tsx
+++ b/client/app/bundles/course/reference-timelines/components/TimelinesStack/Timeline.tsx
@@ -1,10 +1,10 @@
import { ReactNode, useState } from 'react';
import { Add } from '@mui/icons-material';
import { Typography } from '@mui/material';
-import moment from 'moment';
import { TimeData } from 'types/course/referenceTimelines';
import useTranslation from 'lib/hooks/useTranslation';
+import moment from 'lib/moment';
import translations from '../../translations';
import { DAY_WIDTH_PIXELS, getSecondsFromDays } from '../../utils';
diff --git a/client/app/bundles/course/reference-timelines/contexts/LastSavedContext.tsx b/client/app/bundles/course/reference-timelines/contexts/LastSavedContext.tsx
index c868b65d4ba..873433794ad 100644
--- a/client/app/bundles/course/reference-timelines/contexts/LastSavedContext.tsx
+++ b/client/app/bundles/course/reference-timelines/contexts/LastSavedContext.tsx
@@ -6,7 +6,8 @@ import {
useContext,
useState,
} from 'react';
-import moment from 'moment';
+
+import moment from 'lib/moment';
type FetchStatus = 'loading' | 'success' | 'failure';
diff --git a/client/app/bundles/course/reference-timelines/utils.ts b/client/app/bundles/course/reference-timelines/utils.ts
index b10859b880e..9cce968a80b 100644
--- a/client/app/bundles/course/reference-timelines/utils.ts
+++ b/client/app/bundles/course/reference-timelines/utils.ts
@@ -1,6 +1,7 @@
-import moment from 'moment';
import { TimeData } from 'types/course/referenceTimelines';
+import moment from 'lib/moment';
+
const SECONDS_IN_A_DAY = 86_400 as const;
export const DAY_WIDTH_PIXELS = 30 as const;
diff --git a/client/app/bundles/course/reference-timelines/views/DayView/ItemsSidebar.tsx b/client/app/bundles/course/reference-timelines/views/DayView/ItemsSidebar.tsx
index e0821f4979e..12081c2be64 100644
--- a/client/app/bundles/course/reference-timelines/views/DayView/ItemsSidebar.tsx
+++ b/client/app/bundles/course/reference-timelines/views/DayView/ItemsSidebar.tsx
@@ -1,10 +1,11 @@
import { Typography } from '@mui/material';
-import moment from 'moment';
import {
ItemWithTimeData,
TimelineData,
} from 'types/course/referenceTimelines';
+import moment from 'lib/moment';
+
import { getDaysFromSeconds } from '../../utils';
import TimelineSidebarItem from './TimelineSidebarItem';
diff --git a/client/package.json b/client/package.json
index da4c7900aa6..0a6e9980fb0 100644
--- a/client/package.json
+++ b/client/package.json
@@ -181,6 +181,7 @@
"jest-environment-jsdom": "^29.7.0",
"jest-localstorage-mock": "^2.4.26",
"mkdirp": "^3.0.1",
+ "moment-timezone-data-webpack-plugin": "^1.5.1",
"postcss": "^8.4.38",
"postcss-loader": "^8.1.1",
"prettier": "^3.2.5",
diff --git a/client/webpack.prod.js b/client/webpack.prod.js
index 84d484a03c3..348de3d8e0b 100644
--- a/client/webpack.prod.js
+++ b/client/webpack.prod.js
@@ -1,5 +1,6 @@
const { merge } = require('webpack-merge');
const TerserPlugin = require('terser-webpack-plugin');
+const MomentTimezoneDataPlugin = require('moment-timezone-data-webpack-plugin');
const common = require('./webpack.common');
@@ -29,4 +30,5 @@ module.exports = merge(common, {
},
],
},
+ plugins: [new MomentTimezoneDataPlugin({ startYear: 2014 })],
});
diff --git a/client/yarn.lock b/client/yarn.lock
index 1da56906603..8809c61de32 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -5592,7 +5592,7 @@ find-cache-dir@^2.0.0:
make-dir "^2.0.0"
pkg-dir "^3.0.0"
-find-cache-dir@^3.3.2:
+find-cache-dir@^3.0.0, find-cache-dir@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b"
integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==
@@ -8004,6 +8004,14 @@ mkdirp@^3.0.1:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50"
integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==
+moment-timezone-data-webpack-plugin@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/moment-timezone-data-webpack-plugin/-/moment-timezone-data-webpack-plugin-1.5.1.tgz#9d35dfd3768db55058e1e809d77a2b64bd6d03a4"
+ integrity sha512-1le6a35GgYdWMVYFzrfpE/F6Pk4bj0M3QKD6Iv6ba9LqWGoVqHQRHyCTLvLis5E1J98Sz40ET6yhZzMVakwpjg==
+ dependencies:
+ find-cache-dir "^3.0.0"
+ make-dir "^3.0.0"
+
moment-timezone@^0.5.44:
version "0.5.44"
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.44.tgz#a64a4e47b68a43deeab5ae4eb4f82da77cdf595f"