mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 10:07:46 -05:00
feat(ui): add syncable dark mode (#56243)
Co-authored-by: Ahmad Abdolsaheb <ahmad.abdolsaheb@gmail.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> Co-authored-by: Sem Bauke <semboot699@gmail.com>
This commit is contained in:
@@ -14,7 +14,7 @@ import MicrosoftLogo from '../assets/icons/microsoft-logo';
|
||||
import { createFlashMessage } from '../components/Flash/redux';
|
||||
import { Loader } from '../components/helpers';
|
||||
import RedirectHome from '../components/redirect-home';
|
||||
import { Themes } from '../components/settings/theme';
|
||||
import { LocalStorageThemes } from '../redux/types';
|
||||
import { showCert, fetchProfileForUser } from '../redux/actions';
|
||||
import {
|
||||
showCertSelector,
|
||||
@@ -273,7 +273,7 @@ const ShowCertification = (props: ShowCertificationProps): JSX.Element => {
|
||||
data-playwright-test-label='donation-form'
|
||||
>
|
||||
<MultiTierDonationForm
|
||||
defaultTheme={Themes.Default}
|
||||
defaultTheme={LocalStorageThemes.Light}
|
||||
handleProcessing={handleProcessing}
|
||||
isMinimalForm={true}
|
||||
paymentContext={PaymentContext.Certificate}
|
||||
|
||||
@@ -42,7 +42,7 @@ const loggedInProps = {
|
||||
navigate: navigate,
|
||||
showLoading: false,
|
||||
submitNewAbout: jest.fn(),
|
||||
toggleNightMode: jest.fn(),
|
||||
toggleTheme: jest.fn(),
|
||||
updateSocials: jest.fn(),
|
||||
updateIsHonest: jest.fn(),
|
||||
updatePortfolio: jest.fn(),
|
||||
|
||||
@@ -17,7 +17,6 @@ import DangerZone from '../components/settings/danger-zone';
|
||||
import Email from '../components/settings/email';
|
||||
import Honesty from '../components/settings/honesty';
|
||||
import Privacy from '../components/settings/privacy';
|
||||
import { type ThemeProps, Themes } from '../components/settings/theme';
|
||||
import UserToken from '../components/settings/user-token';
|
||||
import ExamToken from '../components/settings/exam-token';
|
||||
import { hardGoTo as navigate } from '../redux/actions';
|
||||
@@ -33,7 +32,6 @@ import {
|
||||
updateMyHonesty,
|
||||
updateMyQuincyEmail,
|
||||
updateMySound,
|
||||
updateMyTheme,
|
||||
updateMyKeyboardShortcuts,
|
||||
verifyCert,
|
||||
resetMyEditorLayout
|
||||
@@ -42,7 +40,7 @@ import {
|
||||
const { apiLocation } = envData;
|
||||
|
||||
// TODO: update types for actions
|
||||
type ShowSettingsProps = Pick<ThemeProps, 'toggleNightMode'> & {
|
||||
type ShowSettingsProps = {
|
||||
createFlashMessage: typeof createFlashMessage;
|
||||
isSignedIn: boolean;
|
||||
navigate: (location: string) => void;
|
||||
@@ -75,7 +73,6 @@ const mapDispatchToProps = {
|
||||
createFlashMessage,
|
||||
navigate,
|
||||
submitNewAbout,
|
||||
toggleNightMode: (theme: Themes) => updateMyTheme({ theme }),
|
||||
toggleSoundMode: (sound: boolean) => updateMySound({ sound }),
|
||||
toggleKeyboardShortcuts: (keyboardShortcuts: boolean) =>
|
||||
updateMyKeyboardShortcuts({ keyboardShortcuts }),
|
||||
@@ -91,7 +88,6 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
|
||||
const {
|
||||
createFlashMessage,
|
||||
isSignedIn,
|
||||
toggleNightMode,
|
||||
toggleSoundMode,
|
||||
toggleKeyboardShortcuts,
|
||||
resetEditorLayout,
|
||||
@@ -121,7 +117,6 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
|
||||
isHonest,
|
||||
sendQuincyEmail,
|
||||
username,
|
||||
theme,
|
||||
keyboardShortcuts
|
||||
},
|
||||
navigate,
|
||||
@@ -163,13 +158,11 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
|
||||
{t('settings.for', { username: username })}
|
||||
</h1>
|
||||
<MiscSettings
|
||||
currentTheme={theme}
|
||||
keyboardShortcuts={keyboardShortcuts}
|
||||
sound={sound}
|
||||
editorLayout={editorLayout}
|
||||
resetEditorLayout={resetEditorLayout}
|
||||
toggleKeyboardShortcuts={toggleKeyboardShortcuts}
|
||||
toggleNightMode={toggleNightMode}
|
||||
toggleSoundMode={toggleSoundMode}
|
||||
/>
|
||||
<Spacer size='m' />
|
||||
|
||||
@@ -21,10 +21,10 @@ import {
|
||||
isDonatingSelector,
|
||||
signInLoadingSelector,
|
||||
donationFormStateSelector,
|
||||
completedChallengesSelector
|
||||
completedChallengesSelector,
|
||||
themeSelector
|
||||
} from '../../redux/selectors';
|
||||
import { Themes } from '../settings/theme';
|
||||
import { DonateFormState } from '../../redux/types';
|
||||
import { LocalStorageThemes, DonateFormState } from '../../redux/types';
|
||||
import type { CompletedChallenge } from '../../redux/prop-types';
|
||||
import { CENTS_IN_DOLLAR, formattedAmountLabel } from './utils';
|
||||
import DonateCompletion from './donate-completion';
|
||||
@@ -61,7 +61,7 @@ type PostCharge = (data: {
|
||||
|
||||
type DonateFormProps = {
|
||||
postCharge: PostCharge;
|
||||
defaultTheme?: Themes;
|
||||
defaultTheme?: LocalStorageThemes;
|
||||
email: string;
|
||||
handleProcessing?: () => void;
|
||||
editAmount?: () => void;
|
||||
@@ -72,10 +72,10 @@ type DonateFormProps = {
|
||||
isDonating: boolean;
|
||||
showLoading: boolean;
|
||||
t: TFunction;
|
||||
theme: Themes;
|
||||
updateDonationFormState: (state: DonationApprovalData) => unknown;
|
||||
paymentContext: PaymentContext;
|
||||
completedChallenges: CompletedChallenge[];
|
||||
theme: LocalStorageThemes;
|
||||
};
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
@@ -85,21 +85,23 @@ const mapStateToProps = createSelector(
|
||||
donationFormStateSelector,
|
||||
userSelector,
|
||||
completedChallengesSelector,
|
||||
themeSelector,
|
||||
(
|
||||
showLoading: DonateFormProps['showLoading'],
|
||||
isSignedIn: DonateFormProps['isSignedIn'],
|
||||
isDonating: DonateFormProps['isDonating'],
|
||||
donationFormState: DonateFormState,
|
||||
{ email, theme }: { email: string; theme: Themes },
|
||||
completedChallenges: CompletedChallenge[]
|
||||
{ email }: { email: string },
|
||||
completedChallenges: CompletedChallenge[],
|
||||
theme: LocalStorageThemes
|
||||
) => ({
|
||||
isSignedIn,
|
||||
isDonating,
|
||||
showLoading,
|
||||
donationFormState,
|
||||
email,
|
||||
theme,
|
||||
completedChallenges
|
||||
completedChallenges,
|
||||
theme
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
defaultTierAmount,
|
||||
type DonationAmount
|
||||
} from '../../../../shared/config/donation-settings'; // You can further extract these into separate components and import them
|
||||
import { Themes } from '../settings/theme';
|
||||
import { LocalStorageThemes } from '../../redux/types';
|
||||
import { formattedAmountLabel, convertToTimeContributed } from './utils';
|
||||
import DonateForm from './donate-form';
|
||||
|
||||
@@ -26,7 +26,7 @@ type MultiTierDonationFormProps = {
|
||||
handleProcessing?: () => void;
|
||||
paymentContext: PaymentContext;
|
||||
isMinimalForm?: boolean;
|
||||
defaultTheme?: Themes;
|
||||
defaultTheme?: LocalStorageThemes;
|
||||
isAnimationEnabled?: boolean;
|
||||
};
|
||||
function SelectionTabs({
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from '../../../../shared/config/donation-settings';
|
||||
import envData from '../../../config/env.json';
|
||||
import { userSelector, signInLoadingSelector } from '../../redux/selectors';
|
||||
import { Themes } from '../settings/theme';
|
||||
import { LocalStorageThemes } from '../../redux/types';
|
||||
import { DonationApprovalData, PostPayment } from './types';
|
||||
import PayPalButtonScriptLoader from './paypal-button-script-loader';
|
||||
|
||||
@@ -34,7 +34,7 @@ type PaypalButtonProps = {
|
||||
isPaypalLoading: boolean;
|
||||
t: (label: string) => string;
|
||||
ref?: Ref<PaypalButton>;
|
||||
theme: Themes;
|
||||
theme: LocalStorageThemes;
|
||||
isSubscription?: boolean;
|
||||
handlePaymentButtonLoad: (provider: 'stripe' | 'paypal') => void;
|
||||
isMinimalForm: boolean | undefined;
|
||||
@@ -91,7 +91,7 @@ class PaypalButton extends Component<PaypalButtonProps, PaypalButtonState> {
|
||||
const { duration, planId, amount } = this.state;
|
||||
const { t, theme, isPaypalLoading, isMinimalForm } = this.props;
|
||||
const isSubscription = duration !== 'one-time';
|
||||
const buttonColor = theme === Themes.Night ? 'white' : 'gold';
|
||||
const buttonColor = theme === LocalStorageThemes.Dark ? 'white' : 'gold';
|
||||
if (!paypalClientId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -11,14 +11,14 @@ import type {
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { PaymentProvider } from '../../../../shared/config/donation-settings';
|
||||
import { Themes } from '../settings/theme';
|
||||
import { LocalStorageThemes } from '../../redux/types';
|
||||
import { DonationApprovalData, PostPayment } from './types';
|
||||
|
||||
interface FormPropTypes {
|
||||
onDonationStateChange: (donationState: DonationApprovalData) => void;
|
||||
postPayment: (arg0: PostPayment) => void;
|
||||
t: (label: string) => string;
|
||||
theme: Themes;
|
||||
theme: LocalStorageThemes;
|
||||
processing: boolean;
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function StripeCardForm({
|
||||
base: {
|
||||
fontSize: '18px',
|
||||
fontFamily: 'Lato, sans-serif',
|
||||
color: `${theme === Themes.Night ? '#fff' : '#0a0a23'}`,
|
||||
color: `${theme === LocalStorageThemes.Dark ? '#fff' : '#0a0a23'}`,
|
||||
'::placeholder': {
|
||||
color: `#858591`
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { PaymentRequest, Stripe } from '@stripe/stripe-js';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Themes } from '../settings/theme';
|
||||
import { LocalStorageThemes } from '../../redux/types';
|
||||
import {
|
||||
PaymentProvider,
|
||||
DonationDuration
|
||||
@@ -17,7 +17,7 @@ import { DonationApprovalData, PostPayment } from './types';
|
||||
interface WrapperProps {
|
||||
label: string;
|
||||
amount: number;
|
||||
theme: Themes;
|
||||
theme: LocalStorageThemes;
|
||||
duration: DonationDuration;
|
||||
postPayment: (arg0: PostPayment) => void;
|
||||
onDonationStateChange: (donationState: DonationApprovalData) => void;
|
||||
@@ -145,7 +145,7 @@ const WalletsButton = ({
|
||||
style: {
|
||||
paymentRequestButton: {
|
||||
type: 'default',
|
||||
theme: theme === Themes.Night ? 'light' : 'dark',
|
||||
theme: theme === LocalStorageThemes.Light ? 'light' : 'dark',
|
||||
height: '43px'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4,10 +4,10 @@ import {
|
||||
FlashState,
|
||||
State,
|
||||
FlashApp,
|
||||
FlashMessageArg
|
||||
FlashMessageArg,
|
||||
LocalStorageThemes
|
||||
} from '../../../redux/types';
|
||||
import { playTone } from '../../../utils/tone';
|
||||
import { Themes } from '../../settings/theme';
|
||||
import { FlashMessages } from './flash-messages';
|
||||
|
||||
export const flashMessageSelector = (state: State): FlashState['message'] =>
|
||||
@@ -33,7 +33,7 @@ export const createFlashMessage = (
|
||||
): ReducerPayload<FlashActionTypes.CreateFlashMessage> => {
|
||||
// Nightmode theme has special tones
|
||||
if (flash.variables?.theme) {
|
||||
void playTone(flash.variables.theme as Themes);
|
||||
void playTone(flash.variables.theme as LocalStorageThemes);
|
||||
} else if (flash.message !== FlashMessages.None) {
|
||||
void playTone(flash.message);
|
||||
}
|
||||
|
||||
@@ -4,31 +4,39 @@ import {
|
||||
faExternalLinkAlt
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import React, { Fragment } from 'react';
|
||||
import React from 'react';
|
||||
import { useTranslation, withTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { radioLocation } from '../../../../config/env.json';
|
||||
import { openSignoutModal } from '../../../redux/actions';
|
||||
import { updateMyTheme } from '../../../redux/settings/actions';
|
||||
import { openSignoutModal, toggleTheme } from '../../../redux/actions';
|
||||
import { Link } from '../../helpers';
|
||||
import { type ThemeProps, Themes } from '../../settings/theme';
|
||||
import { LocalStorageThemes } from '../../../redux/types';
|
||||
import { themeSelector } from '../../../redux/selectors';
|
||||
import { User } from '../../../redux/prop-types';
|
||||
import SupporterBadge from '../../../assets/icons/supporter-badge';
|
||||
|
||||
export interface NavLinksProps extends Pick<ThemeProps, 'toggleNightMode'> {
|
||||
export interface NavLinksProps {
|
||||
displayMenu: boolean;
|
||||
showMenu: () => void;
|
||||
hideMenu: () => void;
|
||||
user?: User;
|
||||
menuButtonRef: React.RefObject<HTMLButtonElement>;
|
||||
openSignoutModal: () => void;
|
||||
theme: LocalStorageThemes;
|
||||
toggleTheme: () => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
toggleNightMode: (theme: Themes) => updateMyTheme({ theme }),
|
||||
toggleTheme,
|
||||
openSignoutModal
|
||||
};
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
themeSelector,
|
||||
(theme: LocalStorageThemes) => ({ theme })
|
||||
);
|
||||
|
||||
interface DonateButtonProps {
|
||||
isUserDonating: boolean | undefined;
|
||||
handleMenuKeyDown: (event: React.KeyboardEvent<HTMLAnchorElement>) => void;
|
||||
@@ -65,29 +73,17 @@ const DonateButton = ({
|
||||
);
|
||||
};
|
||||
|
||||
const toggleTheme = (
|
||||
currentTheme = Themes.Default,
|
||||
toggleNightMode: typeof updateMyTheme
|
||||
) => {
|
||||
toggleNightMode(
|
||||
currentTheme === Themes.Night ? Themes.Default : Themes.Night
|
||||
);
|
||||
};
|
||||
|
||||
function NavLinks({
|
||||
menuButtonRef,
|
||||
openSignoutModal,
|
||||
hideMenu,
|
||||
displayMenu,
|
||||
toggleNightMode,
|
||||
user
|
||||
user,
|
||||
theme,
|
||||
toggleTheme
|
||||
}: NavLinksProps) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
isDonating: isUserDonating,
|
||||
username: currentUserName,
|
||||
theme: currentUserTheme
|
||||
} = user || {};
|
||||
const { isDonating: isUserDonating, username: currentUserName } = user || {};
|
||||
|
||||
// the accessibility tree just needs a little more time to pick up the change.
|
||||
// This function allows us to set aria-expanded to false and then delay just a bit before setting focus on the button
|
||||
@@ -249,40 +245,16 @@ function NavLinks({
|
||||
</li>
|
||||
<li className='nav-line' key='theme'>
|
||||
<button
|
||||
{...(!currentUserName && { 'aria-describedby': 'theme-sign-in' })}
|
||||
aria-disabled={!currentUserName}
|
||||
aria-pressed={currentUserTheme === Themes.Night ? 'true' : 'false'}
|
||||
className={
|
||||
'nav-link nav-link-flex' +
|
||||
(!currentUserName ? ' nav-link-header' : '')
|
||||
}
|
||||
onClick={() => {
|
||||
if (currentUserName) {
|
||||
toggleTheme(currentUserTheme, toggleNightMode);
|
||||
}
|
||||
}}
|
||||
aria-pressed={theme === LocalStorageThemes.Dark}
|
||||
className={'nav-link nav-link-flex'}
|
||||
onClick={toggleTheme}
|
||||
onKeyDown={currentUserName ? handleMenuKeyDown : handleSignOutKeys}
|
||||
>
|
||||
{currentUserName ? (
|
||||
<>
|
||||
<span>{t('settings.labels.night-mode')}</span>
|
||||
{currentUserTheme === Themes.Night ? (
|
||||
<FontAwesomeIcon icon={faCheckSquare} />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faSquare} />
|
||||
)}
|
||||
</>
|
||||
<span>{t('settings.labels.night-mode')}</span>
|
||||
{theme === LocalStorageThemes.Dark ? (
|
||||
<FontAwesomeIcon icon={faCheckSquare} />
|
||||
) : (
|
||||
<Fragment key='night-mode'>
|
||||
<span className='sr-only'>{t('settings.labels.night-mode')}</span>
|
||||
<span
|
||||
aria-hidden='true'
|
||||
className='nav-link-dull'
|
||||
id='theme-sign-in'
|
||||
>
|
||||
{t('misc.change-theme')}
|
||||
</span>
|
||||
</Fragment>
|
||||
<FontAwesomeIcon icon={faSquare} />
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
@@ -304,4 +276,7 @@ function NavLinks({
|
||||
|
||||
NavLinks.displayName = 'NavLinks';
|
||||
|
||||
export default connect(null, mapDispatchToProps)(withTranslation()(NavLinks));
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withTranslation()(NavLinks));
|
||||
|
||||
@@ -19,7 +19,7 @@ const SearchBarOptimized = Loadable(
|
||||
|
||||
type UniversalNavProps = Omit<
|
||||
NavLinksProps,
|
||||
'toggleNightMode' | 'openSignoutModal'
|
||||
'toggleTheme' | 'openSignoutModal'
|
||||
> & {
|
||||
fetchState: { pending: boolean };
|
||||
searchBarRef?: React.RefObject<HTMLDivElement>;
|
||||
|
||||
@@ -23,6 +23,7 @@ import hackZeroSlashRegularURL from '../../../static/fonts/hack-zeroslash/Hack-Z
|
||||
import { isBrowser } from '../../../utils';
|
||||
import {
|
||||
fetchUser,
|
||||
initializeTheme,
|
||||
onlineStatusChange,
|
||||
serverStatusChange
|
||||
} from '../../redux/actions';
|
||||
@@ -32,7 +33,8 @@ import {
|
||||
userSelector,
|
||||
isOnlineSelector,
|
||||
isServerOnlineSelector,
|
||||
userFetchStateSelector
|
||||
userFetchStateSelector,
|
||||
themeSelector
|
||||
} from '../../redux/selectors';
|
||||
|
||||
import { UserFetchState, User } from '../../redux/prop-types';
|
||||
@@ -56,6 +58,7 @@ import './fonts.css';
|
||||
import './global.css';
|
||||
import './variables.css';
|
||||
import './rtl-layout.css';
|
||||
import { LocalStorageThemes } from '../../redux/types';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
isSignedInSelector,
|
||||
@@ -65,6 +68,7 @@ const mapStateToProps = createSelector(
|
||||
isServerOnlineSelector,
|
||||
userFetchStateSelector,
|
||||
userSelector,
|
||||
themeSelector,
|
||||
(
|
||||
isSignedIn,
|
||||
examInProgress: boolean,
|
||||
@@ -72,7 +76,8 @@ const mapStateToProps = createSelector(
|
||||
isOnline: boolean,
|
||||
isServerOnline: boolean,
|
||||
fetchState: UserFetchState,
|
||||
user: User
|
||||
user: User,
|
||||
theme: LocalStorageThemes
|
||||
) => ({
|
||||
isSignedIn,
|
||||
examInProgress,
|
||||
@@ -81,8 +86,8 @@ const mapStateToProps = createSelector(
|
||||
isOnline,
|
||||
isServerOnline,
|
||||
fetchState,
|
||||
theme: user.theme,
|
||||
user
|
||||
user,
|
||||
theme
|
||||
})
|
||||
);
|
||||
|
||||
@@ -94,7 +99,8 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
fetchUser,
|
||||
removeFlashMessage,
|
||||
onlineStatusChange,
|
||||
serverStatusChange
|
||||
serverStatusChange,
|
||||
initializeTheme
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
@@ -112,13 +118,6 @@ interface DefaultLayoutProps extends StateProps, DispatchProps {
|
||||
superBlock?: string;
|
||||
}
|
||||
|
||||
const getSystemTheme = () =>
|
||||
`${
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches === true
|
||||
? 'dark-palette'
|
||||
: 'light-palette'
|
||||
}`;
|
||||
|
||||
function DefaultLayout({
|
||||
children,
|
||||
hasMessage,
|
||||
@@ -137,7 +136,8 @@ function DefaultLayout({
|
||||
theme,
|
||||
user,
|
||||
pathname,
|
||||
fetchUser
|
||||
fetchUser,
|
||||
initializeTheme
|
||||
}: DefaultLayoutProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const isMobileLayout = useMediaQuery({ maxWidth: MAX_MOBILE_WIDTH });
|
||||
@@ -148,6 +148,12 @@ function DefaultLayout({
|
||||
const isExSmallViewportHeight = useMediaQuery({
|
||||
maxHeight: EX_SMALL_VIEWPORT_HEIGHT
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
initializeTheme();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// componentDidMount
|
||||
if (!isSignedIn) {
|
||||
@@ -170,8 +176,6 @@ function DefaultLayout({
|
||||
return typeof isOnline === 'boolean' ? onlineStatusChange(isOnline) : null;
|
||||
};
|
||||
|
||||
const useSystemTheme = fetchState.complete && isSignedIn === false;
|
||||
|
||||
const isJapanese = clientLocale === 'japanese';
|
||||
|
||||
if (fetchState.pending) {
|
||||
@@ -183,9 +187,7 @@ function DefaultLayout({
|
||||
envData.environment === 'production' && <StagingWarningModal />}
|
||||
<Helmet
|
||||
bodyAttributes={{
|
||||
class: useSystemTheme
|
||||
? getSystemTheme()
|
||||
: `${String(theme) === 'night' ? 'dark' : 'light'}-palette`
|
||||
class: `${theme}-palette`
|
||||
}}
|
||||
meta={[
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { createStore } from 'redux';
|
||||
import { Themes } from '../settings/theme';
|
||||
import { UserThemes } from '../../redux/types';
|
||||
import Profile from './profile';
|
||||
|
||||
jest.mock('../../analytics');
|
||||
@@ -46,7 +46,7 @@ const userProps = {
|
||||
sendQuincyEmail: true,
|
||||
sound: true,
|
||||
keyboardShortcuts: false,
|
||||
theme: Themes.Default,
|
||||
theme: UserThemes.Default,
|
||||
twitter: 'string',
|
||||
username: 'string',
|
||||
website: 'string',
|
||||
|
||||
@@ -2,30 +2,26 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Spacer } from '@freecodecamp/ui';
|
||||
import { FullWidthRow } from '../helpers';
|
||||
import ThemeSettings, { ThemeProps } from '../../components/settings/theme';
|
||||
|
||||
import SoundSettings from '../../components/settings/sound';
|
||||
import KeyboardShortcutsSettings from '../../components/settings/keyboard-shortcuts';
|
||||
import ScrollbarWidthSettings from '../../components/settings/scrollbar-width';
|
||||
|
||||
type MiscSettingsProps = ThemeProps & {
|
||||
currentTheme: string;
|
||||
type MiscSettingsProps = {
|
||||
keyboardShortcuts: boolean;
|
||||
sound: boolean;
|
||||
editorLayout: boolean | null;
|
||||
toggleKeyboardShortcuts: (keyboardShortcuts: boolean) => void;
|
||||
toggleNightMode: () => void;
|
||||
toggleSoundMode: (sound: boolean) => void;
|
||||
resetEditorLayout: () => void;
|
||||
};
|
||||
|
||||
const MiscSettings = ({
|
||||
currentTheme,
|
||||
keyboardShortcuts,
|
||||
sound,
|
||||
editorLayout,
|
||||
resetEditorLayout,
|
||||
toggleKeyboardShortcuts,
|
||||
toggleNightMode,
|
||||
toggleSoundMode
|
||||
}: MiscSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -34,10 +30,6 @@ const MiscSettings = ({
|
||||
<>
|
||||
<Spacer size='m' />
|
||||
<FullWidthRow>
|
||||
<ThemeSettings
|
||||
currentTheme={currentTheme}
|
||||
toggleNightMode={toggleNightMode}
|
||||
/>
|
||||
<SoundSettings sound={sound} toggleSoundMode={toggleSoundMode} />
|
||||
<KeyboardShortcutsSettings
|
||||
keyboardShortcuts={keyboardShortcuts}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { updateMyTheme } from '../../redux/settings/actions';
|
||||
|
||||
import ToggleButtonSetting from './toggle-button-setting';
|
||||
|
||||
export enum Themes {
|
||||
Night = 'night',
|
||||
Default = 'default'
|
||||
}
|
||||
|
||||
export type ThemeProps = {
|
||||
currentTheme: Themes;
|
||||
toggleNightMode: typeof updateMyTheme;
|
||||
};
|
||||
|
||||
export default function ThemeSettings({
|
||||
currentTheme,
|
||||
toggleNightMode
|
||||
}: ThemeProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ToggleButtonSetting
|
||||
action={t('settings.labels.night-mode')}
|
||||
flag={currentTheme === Themes.Night}
|
||||
flagName='currentTheme'
|
||||
offLabel={t('buttons.off')}
|
||||
onLabel={t('buttons.on')}
|
||||
toggleFlag={() => {
|
||||
toggleNightMode(
|
||||
currentTheme === Themes.Night ? Themes.Default : Themes.Night
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ThemeSettings.displayName = 'ThemeSettings';
|
||||
@@ -4,6 +4,9 @@ export const ns = 'app';
|
||||
|
||||
export const actionTypes = createTypes(
|
||||
[
|
||||
'setTheme',
|
||||
'initializeTheme',
|
||||
'toggleTheme',
|
||||
'appMount',
|
||||
'hardGoTo',
|
||||
'allowBlockDonationRequests',
|
||||
|
||||
@@ -52,6 +52,10 @@ export const fetchUser = createAction(actionTypes.fetchUser);
|
||||
export const fetchUserComplete = createAction(actionTypes.fetchUserComplete);
|
||||
export const fetchUserError = createAction(actionTypes.fetchUserError);
|
||||
|
||||
export const toggleTheme = createAction(actionTypes.toggleTheme);
|
||||
export const setTheme = createAction(actionTypes.setTheme);
|
||||
export const initializeTheme = createAction(actionTypes.initializeTheme);
|
||||
|
||||
export const updateAllChallengesInfo = createAction(
|
||||
actionTypes.updateAllChallengesInfo
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ import { createUserTokenSaga } from './user-token-saga';
|
||||
import { createMsUsernameSaga } from './ms-username-saga';
|
||||
import { createSurveySaga } from './survey-saga';
|
||||
import { createSessionCompletedChallengesSaga } from './session-completed-challenges';
|
||||
import { createThemeSaga } from './theme-saga';
|
||||
|
||||
const defaultFetchState = {
|
||||
pending: true,
|
||||
@@ -55,6 +56,7 @@ const initialState = {
|
||||
currentChallengeId: store.get(CURRENT_CHALLENGE_KEY),
|
||||
examInProgress: false,
|
||||
isProcessing: false,
|
||||
theme: 'light',
|
||||
showCert: {},
|
||||
showCertFetchState: {
|
||||
...defaultFetchState
|
||||
@@ -87,6 +89,7 @@ export const epics = [hardGoToEpic, failedUpdatesEpic, updateCompleteEpic];
|
||||
|
||||
export const sagas = [
|
||||
...createAcceptTermsSaga(actionTypes),
|
||||
...createThemeSaga(actionTypes),
|
||||
...createAppMountSaga(actionTypes),
|
||||
...createDonationSaga(actionTypes),
|
||||
...createFetchUserSaga(actionTypes),
|
||||
@@ -253,6 +256,10 @@ export const reducer = handleActions(
|
||||
error: payload
|
||||
}
|
||||
}),
|
||||
[actionTypes.setTheme]: (state, { payload: theme }) => ({
|
||||
...state,
|
||||
theme
|
||||
}),
|
||||
[actionTypes.onlineStatusChange]: (state, { payload: isOnline }) => ({
|
||||
...state,
|
||||
isOnline
|
||||
@@ -481,8 +488,6 @@ export const reducer = handleActions(
|
||||
payload ? spreadThePayloadOnUser(state, payload) : state,
|
||||
[settingsTypes.updateMySoundComplete]: (state, { payload }) =>
|
||||
payload ? spreadThePayloadOnUser(state, payload) : state,
|
||||
[settingsTypes.updateMyThemeComplete]: (state, { payload }) =>
|
||||
payload ? spreadThePayloadOnUser(state, payload) : state,
|
||||
[settingsTypes.updateMyKeyboardShortcutsComplete]: (state, { payload }) =>
|
||||
payload ? spreadThePayloadOnUser(state, payload) : state,
|
||||
[settingsTypes.updateMyHonestyComplete]: (state, { payload }) =>
|
||||
|
||||
@@ -2,8 +2,8 @@ import { HandlerProps } from 'react-reflex';
|
||||
import { SuperBlocks } from '../../../shared/config/curriculum';
|
||||
import { BlockLayouts, BlockTypes } from '../../../shared/config/blocks';
|
||||
import type { ChallengeFile, Ext } from '../../../shared/utils/polyvinyl';
|
||||
import { Themes } from '../components/settings/theme';
|
||||
import { type CertTitle } from '../../config/cert-and-project-map';
|
||||
import { UserThemes } from './types';
|
||||
|
||||
export type { ChallengeFile, Ext };
|
||||
|
||||
@@ -316,7 +316,7 @@ export type User = {
|
||||
savedChallenges: SavedChallenges;
|
||||
sendQuincyEmail: boolean;
|
||||
sound: boolean;
|
||||
theme: Themes;
|
||||
theme: UserThemes;
|
||||
keyboardShortcuts: boolean;
|
||||
twitter: string;
|
||||
username: string;
|
||||
|
||||
@@ -259,6 +259,10 @@ export const allChallengesInfoSelector = state =>
|
||||
export const userProfileFetchStateSelector = state =>
|
||||
state[MainApp].userProfileFetchState;
|
||||
export const usernameSelector = state => state[MainApp].appUsername;
|
||||
export const themeSelector = state => state[MainApp].theme;
|
||||
export const userThemeSelector = state => {
|
||||
return userSelector(state).theme;
|
||||
};
|
||||
export const userSelector = state => {
|
||||
const username = usernameSelector(state);
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ export const actionTypes = createTypes(
|
||||
...createAsyncTypes('updateMyEmail'),
|
||||
...createAsyncTypes('updateMySocials'),
|
||||
...createAsyncTypes('updateMySound'),
|
||||
...createAsyncTypes('updateMyTheme'),
|
||||
...createAsyncTypes('updateMyKeyboardShortcuts'),
|
||||
...createAsyncTypes('updateMyHonesty'),
|
||||
...createAsyncTypes('updateMyQuincyEmail'),
|
||||
|
||||
@@ -46,13 +46,6 @@ export const updateMySoundComplete = createAction(
|
||||
);
|
||||
export const updateMySoundError = createAction(types.updateMySoundError);
|
||||
|
||||
export const updateMyTheme = createAction(types.updateMyTheme);
|
||||
export const updateMyThemeComplete = createAction(
|
||||
types.updateMyThemeComplete,
|
||||
checkForSuccessPayload
|
||||
);
|
||||
export const updateMyThemeError = createAction(types.updateMyThemeError);
|
||||
|
||||
export const updateMyKeyboardShortcuts = createAction(
|
||||
types.updateMyKeyboardShortcuts
|
||||
);
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
putUpdateMyProfileUI,
|
||||
putUpdateMyQuincyEmail,
|
||||
putUpdateMySocials,
|
||||
putUpdateMyTheme,
|
||||
putUpdateMyUsername,
|
||||
putVerifyCert
|
||||
} from '../../utils/ajax';
|
||||
@@ -50,8 +49,6 @@ import {
|
||||
updateMySocialsError,
|
||||
updateMySoundComplete,
|
||||
updateMySoundError,
|
||||
updateMyThemeComplete,
|
||||
updateMyThemeError,
|
||||
validateUsernameComplete,
|
||||
validateUsernameError,
|
||||
verifyCertComplete,
|
||||
@@ -132,16 +129,6 @@ function* resetMyEditorLayoutSaga() {
|
||||
}
|
||||
}
|
||||
|
||||
function* updateMyThemeSaga({ payload: update }) {
|
||||
try {
|
||||
const { data } = yield call(putUpdateMyTheme, update);
|
||||
yield put(updateMyThemeComplete({ ...data, payload: update }));
|
||||
yield put(createFlashMessage({ ...data }));
|
||||
} catch (e) {
|
||||
yield put(updateMyThemeError);
|
||||
}
|
||||
}
|
||||
|
||||
function* updateMyKeyboardShortcutsSaga({ payload: update }) {
|
||||
try {
|
||||
const { data } = yield call(putUpdateMyKeyboardShortcuts, update);
|
||||
@@ -248,7 +235,6 @@ export function createSettingsSagas(types) {
|
||||
takeEvery(types.updateMyHonesty, updateMyHonestySaga),
|
||||
takeEvery(types.updateMySound, updateMySoundSaga),
|
||||
takeEvery(types.resetMyEditorLayout, resetMyEditorLayoutSaga),
|
||||
takeEvery(types.updateMyTheme, updateMyThemeSaga),
|
||||
takeEvery(types.updateMyKeyboardShortcuts, updateMyKeyboardShortcutsSaga),
|
||||
takeEvery(types.updateMyQuincyEmail, updateMyQuincyEmailSaga),
|
||||
takeEvery(types.updateMyPortfolio, updateMyPortfolioSaga),
|
||||
|
||||
45
client/src/redux/theme-saga.js
Normal file
45
client/src/redux/theme-saga.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { put, takeEvery, select, take } from 'redux-saga/effects';
|
||||
import { createFlashMessage } from '../components/Flash/redux';
|
||||
import { setTheme } from './actions';
|
||||
import { actionTypes } from './action-types';
|
||||
import { userThemeSelector } from './selectors';
|
||||
|
||||
function* toggleThemeSaga() {
|
||||
const data = { type: 'success', message: 'flash.updated-themes' };
|
||||
const currentTheme = localStorage.getItem('theme');
|
||||
const invertedTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
localStorage.setItem('theme', invertedTheme);
|
||||
yield put(setTheme(invertedTheme));
|
||||
yield put(createFlashMessage({ ...data }));
|
||||
}
|
||||
|
||||
function* initializeThemeSaga() {
|
||||
// Wait for the fetch userComplete action
|
||||
yield take(actionTypes.fetchUserComplete);
|
||||
|
||||
const userTheme = yield select(userThemeSelector);
|
||||
const localStorageTheme = localStorage.getItem('theme');
|
||||
const isSysThemeDark = window.matchMedia(
|
||||
'(prefers-color-scheme: dark)'
|
||||
).matches;
|
||||
|
||||
let selectTheme = 'light';
|
||||
|
||||
if (localStorageTheme !== null) {
|
||||
selectTheme = localStorageTheme === 'dark' ? 'dark' : 'light';
|
||||
} else if (userTheme) {
|
||||
selectTheme = userTheme === 'night' ? 'dark' : 'light';
|
||||
} else if (isSysThemeDark) {
|
||||
selectTheme = 'dark';
|
||||
}
|
||||
|
||||
localStorage.setItem('theme', selectTheme);
|
||||
yield put(setTheme(selectTheme));
|
||||
}
|
||||
|
||||
export function createThemeSaga(types) {
|
||||
return [
|
||||
takeEvery(types.toggleTheme, toggleThemeSaga),
|
||||
takeEvery(types.initializeTheme, initializeThemeSaga)
|
||||
];
|
||||
}
|
||||
@@ -62,3 +62,13 @@ export interface UpdateCardState {
|
||||
success: boolean;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export enum LocalStorageThemes {
|
||||
Light = 'light',
|
||||
Dark = 'dark'
|
||||
}
|
||||
|
||||
export enum UserThemes {
|
||||
Night = 'night',
|
||||
Default = 'default'
|
||||
}
|
||||
|
||||
@@ -18,12 +18,12 @@ import store from 'store';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Loader } from '../../../components/helpers';
|
||||
import { Themes } from '../../../components/settings/theme';
|
||||
import { LocalStorageThemes } from '../../../redux/types';
|
||||
import { saveChallenge } from '../../../redux/actions';
|
||||
import {
|
||||
isDonationModalOpenSelector,
|
||||
isSignedInSelector,
|
||||
userSelector
|
||||
themeSelector
|
||||
} from '../../../redux/selectors';
|
||||
import {
|
||||
ChallengeFiles,
|
||||
@@ -101,7 +101,7 @@ export interface EditorProps {
|
||||
stopResetting: () => void;
|
||||
resetAttempts: () => void;
|
||||
tests: Test[];
|
||||
theme: Themes;
|
||||
theme: LocalStorageThemes;
|
||||
title: string;
|
||||
showProjectPreview: boolean;
|
||||
previewOpen: boolean;
|
||||
@@ -137,9 +137,9 @@ const mapStateToProps = createSelector(
|
||||
isProjectPreviewModalOpenSelector,
|
||||
isResettingSelector,
|
||||
isSignedInSelector,
|
||||
userSelector,
|
||||
challengeTestsSelector,
|
||||
isChallengeCompletedSelector,
|
||||
themeSelector,
|
||||
(
|
||||
attempts: number,
|
||||
canFocus: boolean,
|
||||
@@ -148,9 +148,9 @@ const mapStateToProps = createSelector(
|
||||
previewOpen: boolean,
|
||||
isResetting: boolean,
|
||||
isSignedIn: boolean,
|
||||
{ theme }: { theme: Themes },
|
||||
tests: [{ text: string; testString: string; message?: string }],
|
||||
isChallengeCompleted: boolean
|
||||
isChallengeCompleted: boolean,
|
||||
theme: LocalStorageThemes
|
||||
) => ({
|
||||
attempts,
|
||||
canFocus: open ? false : canFocus,
|
||||
@@ -158,9 +158,9 @@ const mapStateToProps = createSelector(
|
||||
previewOpen,
|
||||
isResetting,
|
||||
isSignedIn,
|
||||
theme,
|
||||
tests,
|
||||
isChallengeCompleted
|
||||
isChallengeCompleted,
|
||||
theme
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1269,9 +1269,9 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
).matches;
|
||||
const editorSystemTheme = preferDarkScheme ? 'vs-dark-custom' : 'vs-custom';
|
||||
const editorTheme =
|
||||
theme === Themes.Night
|
||||
theme === LocalStorageThemes.Dark
|
||||
? 'vs-dark-custom'
|
||||
: theme === Themes.Default
|
||||
: theme === LocalStorageThemes.Light
|
||||
? 'vs-custom'
|
||||
: editorSystemTheme;
|
||||
|
||||
|
||||
@@ -2,10 +2,7 @@ import React, { useRef } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex';
|
||||
import { createSelector } from 'reselect';
|
||||
import {
|
||||
userSelector,
|
||||
isDonationModalOpenSelector
|
||||
} from '../../../redux/selectors';
|
||||
import { isDonationModalOpenSelector } from '../../../redux/selectors';
|
||||
import {
|
||||
canFocusEditorSelector,
|
||||
consoleOutputSelector,
|
||||
@@ -14,7 +11,6 @@ import {
|
||||
import { getTargetEditor } from '../utils/get-target-editor';
|
||||
import './editor.css';
|
||||
import { FileKey } from '../../../redux/prop-types';
|
||||
import { Themes } from '../../../components/settings/theme';
|
||||
import Editor, { type EditorProps } from './editor';
|
||||
|
||||
export type VisibleEditors = {
|
||||
@@ -51,18 +47,15 @@ const mapStateToProps = createSelector(
|
||||
canFocusEditorSelector,
|
||||
consoleOutputSelector,
|
||||
isDonationModalOpenSelector,
|
||||
userSelector,
|
||||
(
|
||||
visibleEditors: VisibleEditors,
|
||||
canFocus: boolean,
|
||||
output: string[],
|
||||
open,
|
||||
{ theme }: { theme: Themes }
|
||||
open
|
||||
) => ({
|
||||
visibleEditors,
|
||||
canFocus: open ? false : canFocus,
|
||||
output,
|
||||
theme
|
||||
output
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -363,12 +363,6 @@ export function putUpdateMySocials(
|
||||
return put('/update-my-socials', update);
|
||||
}
|
||||
|
||||
export function putUpdateMyTheme(
|
||||
update: Record<string, string>
|
||||
): Promise<ResponseWithData<void>> {
|
||||
return put('/update-my-theme', update);
|
||||
}
|
||||
|
||||
export function putUpdateMyKeyboardShortcuts(
|
||||
update: Record<string, string>
|
||||
): Promise<ResponseWithData<void>> {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import store from 'store';
|
||||
import { FlashMessages } from '../../components/Flash/redux/flash-messages';
|
||||
import { Themes } from '../../components/settings/theme';
|
||||
|
||||
import { LocalStorageThemes } from '../../redux/types';
|
||||
const TRY_AGAIN = 'https://campfire-mode.freecodecamp.org/try-again.mp3';
|
||||
const CHAL_COMP = 'https://campfire-mode.freecodecamp.org/chal-comp.mp3';
|
||||
|
||||
const toneUrls = {
|
||||
[Themes.Default]: 'https://campfire-mode.freecodecamp.org/day.mp3',
|
||||
[Themes.Night]: 'https://campfire-mode.freecodecamp.org/night.mp3',
|
||||
[LocalStorageThemes.Light]: 'https://campfire-mode.freecodecamp.org/day.mp3',
|
||||
[LocalStorageThemes.Dark]: 'https://campfire-mode.freecodecamp.org/night.mp3',
|
||||
donation: 'https://campfire-mode.freecodecamp.org/donate.mp3',
|
||||
'tests-completed': CHAL_COMP,
|
||||
'block-toggle': 'https://tonejs.github.io/audio/berklee/guitar_chord1.mp3',
|
||||
|
||||
@@ -79,7 +79,7 @@ test.describe('Editor theme if the system theme is dark', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('If the user is signed out', () => {
|
||||
test.describe('If the user is signed out and has no local storage data', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('should be in dark mode', async ({ page }) => {
|
||||
@@ -88,6 +88,27 @@ test.describe('Editor theme if the system theme is dark', () => {
|
||||
await expect(editor).toHaveClass(/vs-dark/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('if the user is signed out and has a dark theme set in local storage', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('should be in dark mode', async ({ page }) => {
|
||||
// go to the test page
|
||||
await page.goto(testPage);
|
||||
|
||||
// set the dark theme in local storage
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('theme', 'dark');
|
||||
});
|
||||
|
||||
// reload the page to apply the local storage changes
|
||||
await page.reload();
|
||||
|
||||
// check if the editor is in dark mode
|
||||
const editor = page.locator("div[role='code'].monaco-editor");
|
||||
await expect(editor).toHaveClass(/vs-dark/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Editor theme if the system theme is light', () => {
|
||||
@@ -135,7 +156,7 @@ test.describe('Editor theme if the system theme is light', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('If the user is signed out', () => {
|
||||
test.describe('If the user is signed out and has no local storage value', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('should be in light mode', async ({ page }) => {
|
||||
@@ -143,5 +164,26 @@ test.describe('Editor theme if the system theme is light', () => {
|
||||
const editor = page.locator("div[role='code'].monaco-editor");
|
||||
await expect(editor).toHaveClass(/vs(?!\w)/);
|
||||
});
|
||||
|
||||
test.describe('if the user is signed out and has a light theme set in local storage', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('should be in light mode', async ({ page }) => {
|
||||
// go to the test page
|
||||
await page.goto(testPage);
|
||||
|
||||
// set the light theme in local storage
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('theme', 'light');
|
||||
});
|
||||
|
||||
// reload the page to apply the local storage changes
|
||||
await page.reload();
|
||||
|
||||
// check if the editor is in light mode
|
||||
const editor = page.locator("div[role='code'].monaco-editor");
|
||||
await expect(editor).toHaveClass(/vs(?!\w)/);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -168,15 +168,6 @@ test.describe('Settings - Certified User', () => {
|
||||
name: translations.buttons['download-data']
|
||||
});
|
||||
await expect(downloadButton).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page
|
||||
.getByRole('group', {
|
||||
name: translations.settings.labels['night-mode'],
|
||||
exact: true
|
||||
})
|
||||
.locator('p')
|
||||
).toBeVisible();
|
||||
await expect(page.locator('#legendsound')).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(translations.settings['sound-volume'])
|
||||
|
||||
Reference in New Issue
Block a user