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:
Anna
2024-12-10 10:42:09 -05:00
committed by GitHub
parent 674a31811e
commit 0b77e59457
31 changed files with 218 additions and 225 deletions

View File

@@ -14,7 +14,7 @@ import MicrosoftLogo from '../assets/icons/microsoft-logo';
import { createFlashMessage } from '../components/Flash/redux'; import { createFlashMessage } from '../components/Flash/redux';
import { Loader } from '../components/helpers'; import { Loader } from '../components/helpers';
import RedirectHome from '../components/redirect-home'; import RedirectHome from '../components/redirect-home';
import { Themes } from '../components/settings/theme'; import { LocalStorageThemes } from '../redux/types';
import { showCert, fetchProfileForUser } from '../redux/actions'; import { showCert, fetchProfileForUser } from '../redux/actions';
import { import {
showCertSelector, showCertSelector,
@@ -273,7 +273,7 @@ const ShowCertification = (props: ShowCertificationProps): JSX.Element => {
data-playwright-test-label='donation-form' data-playwright-test-label='donation-form'
> >
<MultiTierDonationForm <MultiTierDonationForm
defaultTheme={Themes.Default} defaultTheme={LocalStorageThemes.Light}
handleProcessing={handleProcessing} handleProcessing={handleProcessing}
isMinimalForm={true} isMinimalForm={true}
paymentContext={PaymentContext.Certificate} paymentContext={PaymentContext.Certificate}

View File

@@ -42,7 +42,7 @@ const loggedInProps = {
navigate: navigate, navigate: navigate,
showLoading: false, showLoading: false,
submitNewAbout: jest.fn(), submitNewAbout: jest.fn(),
toggleNightMode: jest.fn(), toggleTheme: jest.fn(),
updateSocials: jest.fn(), updateSocials: jest.fn(),
updateIsHonest: jest.fn(), updateIsHonest: jest.fn(),
updatePortfolio: jest.fn(), updatePortfolio: jest.fn(),

View File

@@ -17,7 +17,6 @@ import DangerZone from '../components/settings/danger-zone';
import Email from '../components/settings/email'; import Email from '../components/settings/email';
import Honesty from '../components/settings/honesty'; import Honesty from '../components/settings/honesty';
import Privacy from '../components/settings/privacy'; import Privacy from '../components/settings/privacy';
import { type ThemeProps, Themes } from '../components/settings/theme';
import UserToken from '../components/settings/user-token'; import UserToken from '../components/settings/user-token';
import ExamToken from '../components/settings/exam-token'; import ExamToken from '../components/settings/exam-token';
import { hardGoTo as navigate } from '../redux/actions'; import { hardGoTo as navigate } from '../redux/actions';
@@ -33,7 +32,6 @@ import {
updateMyHonesty, updateMyHonesty,
updateMyQuincyEmail, updateMyQuincyEmail,
updateMySound, updateMySound,
updateMyTheme,
updateMyKeyboardShortcuts, updateMyKeyboardShortcuts,
verifyCert, verifyCert,
resetMyEditorLayout resetMyEditorLayout
@@ -42,7 +40,7 @@ import {
const { apiLocation } = envData; const { apiLocation } = envData;
// TODO: update types for actions // TODO: update types for actions
type ShowSettingsProps = Pick<ThemeProps, 'toggleNightMode'> & { type ShowSettingsProps = {
createFlashMessage: typeof createFlashMessage; createFlashMessage: typeof createFlashMessage;
isSignedIn: boolean; isSignedIn: boolean;
navigate: (location: string) => void; navigate: (location: string) => void;
@@ -75,7 +73,6 @@ const mapDispatchToProps = {
createFlashMessage, createFlashMessage,
navigate, navigate,
submitNewAbout, submitNewAbout,
toggleNightMode: (theme: Themes) => updateMyTheme({ theme }),
toggleSoundMode: (sound: boolean) => updateMySound({ sound }), toggleSoundMode: (sound: boolean) => updateMySound({ sound }),
toggleKeyboardShortcuts: (keyboardShortcuts: boolean) => toggleKeyboardShortcuts: (keyboardShortcuts: boolean) =>
updateMyKeyboardShortcuts({ keyboardShortcuts }), updateMyKeyboardShortcuts({ keyboardShortcuts }),
@@ -91,7 +88,6 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
const { const {
createFlashMessage, createFlashMessage,
isSignedIn, isSignedIn,
toggleNightMode,
toggleSoundMode, toggleSoundMode,
toggleKeyboardShortcuts, toggleKeyboardShortcuts,
resetEditorLayout, resetEditorLayout,
@@ -121,7 +117,6 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
isHonest, isHonest,
sendQuincyEmail, sendQuincyEmail,
username, username,
theme,
keyboardShortcuts keyboardShortcuts
}, },
navigate, navigate,
@@ -163,13 +158,11 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
{t('settings.for', { username: username })} {t('settings.for', { username: username })}
</h1> </h1>
<MiscSettings <MiscSettings
currentTheme={theme}
keyboardShortcuts={keyboardShortcuts} keyboardShortcuts={keyboardShortcuts}
sound={sound} sound={sound}
editorLayout={editorLayout} editorLayout={editorLayout}
resetEditorLayout={resetEditorLayout} resetEditorLayout={resetEditorLayout}
toggleKeyboardShortcuts={toggleKeyboardShortcuts} toggleKeyboardShortcuts={toggleKeyboardShortcuts}
toggleNightMode={toggleNightMode}
toggleSoundMode={toggleSoundMode} toggleSoundMode={toggleSoundMode}
/> />
<Spacer size='m' /> <Spacer size='m' />

View File

@@ -21,10 +21,10 @@ import {
isDonatingSelector, isDonatingSelector,
signInLoadingSelector, signInLoadingSelector,
donationFormStateSelector, donationFormStateSelector,
completedChallengesSelector completedChallengesSelector,
themeSelector
} from '../../redux/selectors'; } from '../../redux/selectors';
import { Themes } from '../settings/theme'; import { LocalStorageThemes, DonateFormState } from '../../redux/types';
import { DonateFormState } from '../../redux/types';
import type { CompletedChallenge } from '../../redux/prop-types'; import type { CompletedChallenge } from '../../redux/prop-types';
import { CENTS_IN_DOLLAR, formattedAmountLabel } from './utils'; import { CENTS_IN_DOLLAR, formattedAmountLabel } from './utils';
import DonateCompletion from './donate-completion'; import DonateCompletion from './donate-completion';
@@ -61,7 +61,7 @@ type PostCharge = (data: {
type DonateFormProps = { type DonateFormProps = {
postCharge: PostCharge; postCharge: PostCharge;
defaultTheme?: Themes; defaultTheme?: LocalStorageThemes;
email: string; email: string;
handleProcessing?: () => void; handleProcessing?: () => void;
editAmount?: () => void; editAmount?: () => void;
@@ -72,10 +72,10 @@ type DonateFormProps = {
isDonating: boolean; isDonating: boolean;
showLoading: boolean; showLoading: boolean;
t: TFunction; t: TFunction;
theme: Themes;
updateDonationFormState: (state: DonationApprovalData) => unknown; updateDonationFormState: (state: DonationApprovalData) => unknown;
paymentContext: PaymentContext; paymentContext: PaymentContext;
completedChallenges: CompletedChallenge[]; completedChallenges: CompletedChallenge[];
theme: LocalStorageThemes;
}; };
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
@@ -85,21 +85,23 @@ const mapStateToProps = createSelector(
donationFormStateSelector, donationFormStateSelector,
userSelector, userSelector,
completedChallengesSelector, completedChallengesSelector,
themeSelector,
( (
showLoading: DonateFormProps['showLoading'], showLoading: DonateFormProps['showLoading'],
isSignedIn: DonateFormProps['isSignedIn'], isSignedIn: DonateFormProps['isSignedIn'],
isDonating: DonateFormProps['isDonating'], isDonating: DonateFormProps['isDonating'],
donationFormState: DonateFormState, donationFormState: DonateFormState,
{ email, theme }: { email: string; theme: Themes }, { email }: { email: string },
completedChallenges: CompletedChallenge[] completedChallenges: CompletedChallenge[],
theme: LocalStorageThemes
) => ({ ) => ({
isSignedIn, isSignedIn,
isDonating, isDonating,
showLoading, showLoading,
donationFormState, donationFormState,
email, email,
theme, completedChallenges,
completedChallenges theme
}) })
); );

View File

@@ -17,7 +17,7 @@ import {
defaultTierAmount, defaultTierAmount,
type DonationAmount type DonationAmount
} from '../../../../shared/config/donation-settings'; // You can further extract these into separate components and import them } 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 { formattedAmountLabel, convertToTimeContributed } from './utils';
import DonateForm from './donate-form'; import DonateForm from './donate-form';
@@ -26,7 +26,7 @@ type MultiTierDonationFormProps = {
handleProcessing?: () => void; handleProcessing?: () => void;
paymentContext: PaymentContext; paymentContext: PaymentContext;
isMinimalForm?: boolean; isMinimalForm?: boolean;
defaultTheme?: Themes; defaultTheme?: LocalStorageThemes;
isAnimationEnabled?: boolean; isAnimationEnabled?: boolean;
}; };
function SelectionTabs({ function SelectionTabs({

View File

@@ -12,7 +12,7 @@ import {
} from '../../../../shared/config/donation-settings'; } from '../../../../shared/config/donation-settings';
import envData from '../../../config/env.json'; import envData from '../../../config/env.json';
import { userSelector, signInLoadingSelector } from '../../redux/selectors'; import { userSelector, signInLoadingSelector } from '../../redux/selectors';
import { Themes } from '../settings/theme'; import { LocalStorageThemes } from '../../redux/types';
import { DonationApprovalData, PostPayment } from './types'; import { DonationApprovalData, PostPayment } from './types';
import PayPalButtonScriptLoader from './paypal-button-script-loader'; import PayPalButtonScriptLoader from './paypal-button-script-loader';
@@ -34,7 +34,7 @@ type PaypalButtonProps = {
isPaypalLoading: boolean; isPaypalLoading: boolean;
t: (label: string) => string; t: (label: string) => string;
ref?: Ref<PaypalButton>; ref?: Ref<PaypalButton>;
theme: Themes; theme: LocalStorageThemes;
isSubscription?: boolean; isSubscription?: boolean;
handlePaymentButtonLoad: (provider: 'stripe' | 'paypal') => void; handlePaymentButtonLoad: (provider: 'stripe' | 'paypal') => void;
isMinimalForm: boolean | undefined; isMinimalForm: boolean | undefined;
@@ -91,7 +91,7 @@ class PaypalButton extends Component<PaypalButtonProps, PaypalButtonState> {
const { duration, planId, amount } = this.state; const { duration, planId, amount } = this.state;
const { t, theme, isPaypalLoading, isMinimalForm } = this.props; const { t, theme, isPaypalLoading, isMinimalForm } = this.props;
const isSubscription = duration !== 'one-time'; const isSubscription = duration !== 'one-time';
const buttonColor = theme === Themes.Night ? 'white' : 'gold'; const buttonColor = theme === LocalStorageThemes.Dark ? 'white' : 'gold';
if (!paypalClientId) { if (!paypalClientId) {
return null; return null;
} }

View File

@@ -11,14 +11,14 @@ import type {
import React, { useState } from 'react'; import React, { useState } from 'react';
import { PaymentProvider } from '../../../../shared/config/donation-settings'; import { PaymentProvider } from '../../../../shared/config/donation-settings';
import { Themes } from '../settings/theme'; import { LocalStorageThemes } from '../../redux/types';
import { DonationApprovalData, PostPayment } from './types'; import { DonationApprovalData, PostPayment } from './types';
interface FormPropTypes { interface FormPropTypes {
onDonationStateChange: (donationState: DonationApprovalData) => void; onDonationStateChange: (donationState: DonationApprovalData) => void;
postPayment: (arg0: PostPayment) => void; postPayment: (arg0: PostPayment) => void;
t: (label: string) => string; t: (label: string) => string;
theme: Themes; theme: LocalStorageThemes;
processing: boolean; processing: boolean;
} }
@@ -80,7 +80,7 @@ export default function StripeCardForm({
base: { base: {
fontSize: '18px', fontSize: '18px',
fontFamily: 'Lato, sans-serif', fontFamily: 'Lato, sans-serif',
color: `${theme === Themes.Night ? '#fff' : '#0a0a23'}`, color: `${theme === LocalStorageThemes.Dark ? '#fff' : '#0a0a23'}`,
'::placeholder': { '::placeholder': {
color: `#858591` color: `#858591`
} }

View File

@@ -6,7 +6,7 @@ import type { PaymentRequest, Stripe } from '@stripe/stripe-js';
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Themes } from '../settings/theme'; import { LocalStorageThemes } from '../../redux/types';
import { import {
PaymentProvider, PaymentProvider,
DonationDuration DonationDuration
@@ -17,7 +17,7 @@ import { DonationApprovalData, PostPayment } from './types';
interface WrapperProps { interface WrapperProps {
label: string; label: string;
amount: number; amount: number;
theme: Themes; theme: LocalStorageThemes;
duration: DonationDuration; duration: DonationDuration;
postPayment: (arg0: PostPayment) => void; postPayment: (arg0: PostPayment) => void;
onDonationStateChange: (donationState: DonationApprovalData) => void; onDonationStateChange: (donationState: DonationApprovalData) => void;
@@ -145,7 +145,7 @@ const WalletsButton = ({
style: { style: {
paymentRequestButton: { paymentRequestButton: {
type: 'default', type: 'default',
theme: theme === Themes.Night ? 'light' : 'dark', theme: theme === LocalStorageThemes.Light ? 'light' : 'dark',
height: '43px' height: '43px'
} }
}, },

View File

@@ -4,10 +4,10 @@ import {
FlashState, FlashState,
State, State,
FlashApp, FlashApp,
FlashMessageArg FlashMessageArg,
LocalStorageThemes
} from '../../../redux/types'; } from '../../../redux/types';
import { playTone } from '../../../utils/tone'; import { playTone } from '../../../utils/tone';
import { Themes } from '../../settings/theme';
import { FlashMessages } from './flash-messages'; import { FlashMessages } from './flash-messages';
export const flashMessageSelector = (state: State): FlashState['message'] => export const flashMessageSelector = (state: State): FlashState['message'] =>
@@ -33,7 +33,7 @@ export const createFlashMessage = (
): ReducerPayload<FlashActionTypes.CreateFlashMessage> => { ): ReducerPayload<FlashActionTypes.CreateFlashMessage> => {
// Nightmode theme has special tones // Nightmode theme has special tones
if (flash.variables?.theme) { if (flash.variables?.theme) {
void playTone(flash.variables.theme as Themes); void playTone(flash.variables.theme as LocalStorageThemes);
} else if (flash.message !== FlashMessages.None) { } else if (flash.message !== FlashMessages.None) {
void playTone(flash.message); void playTone(flash.message);
} }

View File

@@ -4,31 +4,39 @@ import {
faExternalLinkAlt faExternalLinkAlt
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { Fragment } from 'react'; import React from 'react';
import { useTranslation, withTranslation } from 'react-i18next'; import { useTranslation, withTranslation } from 'react-i18next';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { radioLocation } from '../../../../config/env.json'; import { radioLocation } from '../../../../config/env.json';
import { openSignoutModal } from '../../../redux/actions'; import { openSignoutModal, toggleTheme } from '../../../redux/actions';
import { updateMyTheme } from '../../../redux/settings/actions';
import { Link } from '../../helpers'; 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 { User } from '../../../redux/prop-types';
import SupporterBadge from '../../../assets/icons/supporter-badge'; import SupporterBadge from '../../../assets/icons/supporter-badge';
export interface NavLinksProps extends Pick<ThemeProps, 'toggleNightMode'> { export interface NavLinksProps {
displayMenu: boolean; displayMenu: boolean;
showMenu: () => void; showMenu: () => void;
hideMenu: () => void; hideMenu: () => void;
user?: User; user?: User;
menuButtonRef: React.RefObject<HTMLButtonElement>; menuButtonRef: React.RefObject<HTMLButtonElement>;
openSignoutModal: () => void; openSignoutModal: () => void;
theme: LocalStorageThemes;
toggleTheme: () => void;
} }
const mapDispatchToProps = { const mapDispatchToProps = {
toggleNightMode: (theme: Themes) => updateMyTheme({ theme }), toggleTheme,
openSignoutModal openSignoutModal
}; };
const mapStateToProps = createSelector(
themeSelector,
(theme: LocalStorageThemes) => ({ theme })
);
interface DonateButtonProps { interface DonateButtonProps {
isUserDonating: boolean | undefined; isUserDonating: boolean | undefined;
handleMenuKeyDown: (event: React.KeyboardEvent<HTMLAnchorElement>) => void; 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({ function NavLinks({
menuButtonRef, menuButtonRef,
openSignoutModal, openSignoutModal,
hideMenu, hideMenu,
displayMenu, displayMenu,
toggleNightMode, user,
user theme,
toggleTheme
}: NavLinksProps) { }: NavLinksProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { const { isDonating: isUserDonating, username: currentUserName } = user || {};
isDonating: isUserDonating,
username: currentUserName,
theme: currentUserTheme
} = user || {};
// the accessibility tree just needs a little more time to pick up the change. // 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 // 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>
<li className='nav-line' key='theme'> <li className='nav-line' key='theme'>
<button <button
{...(!currentUserName && { 'aria-describedby': 'theme-sign-in' })} aria-pressed={theme === LocalStorageThemes.Dark}
aria-disabled={!currentUserName} className={'nav-link nav-link-flex'}
aria-pressed={currentUserTheme === Themes.Night ? 'true' : 'false'} onClick={toggleTheme}
className={
'nav-link nav-link-flex' +
(!currentUserName ? ' nav-link-header' : '')
}
onClick={() => {
if (currentUserName) {
toggleTheme(currentUserTheme, toggleNightMode);
}
}}
onKeyDown={currentUserName ? handleMenuKeyDown : handleSignOutKeys} onKeyDown={currentUserName ? handleMenuKeyDown : handleSignOutKeys}
> >
{currentUserName ? ( <span>{t('settings.labels.night-mode')}</span>
<> {theme === LocalStorageThemes.Dark ? (
<span>{t('settings.labels.night-mode')}</span> <FontAwesomeIcon icon={faCheckSquare} />
{currentUserTheme === Themes.Night ? (
<FontAwesomeIcon icon={faCheckSquare} />
) : (
<FontAwesomeIcon icon={faSquare} />
)}
</>
) : ( ) : (
<Fragment key='night-mode'> <FontAwesomeIcon icon={faSquare} />
<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>
)} )}
</button> </button>
</li> </li>
@@ -304,4 +276,7 @@ function NavLinks({
NavLinks.displayName = 'NavLinks'; NavLinks.displayName = 'NavLinks';
export default connect(null, mapDispatchToProps)(withTranslation()(NavLinks)); export default connect(
mapStateToProps,
mapDispatchToProps
)(withTranslation()(NavLinks));

View File

@@ -19,7 +19,7 @@ const SearchBarOptimized = Loadable(
type UniversalNavProps = Omit< type UniversalNavProps = Omit<
NavLinksProps, NavLinksProps,
'toggleNightMode' | 'openSignoutModal' 'toggleTheme' | 'openSignoutModal'
> & { > & {
fetchState: { pending: boolean }; fetchState: { pending: boolean };
searchBarRef?: React.RefObject<HTMLDivElement>; searchBarRef?: React.RefObject<HTMLDivElement>;

View File

@@ -23,6 +23,7 @@ import hackZeroSlashRegularURL from '../../../static/fonts/hack-zeroslash/Hack-Z
import { isBrowser } from '../../../utils'; import { isBrowser } from '../../../utils';
import { import {
fetchUser, fetchUser,
initializeTheme,
onlineStatusChange, onlineStatusChange,
serverStatusChange serverStatusChange
} from '../../redux/actions'; } from '../../redux/actions';
@@ -32,7 +33,8 @@ import {
userSelector, userSelector,
isOnlineSelector, isOnlineSelector,
isServerOnlineSelector, isServerOnlineSelector,
userFetchStateSelector userFetchStateSelector,
themeSelector
} from '../../redux/selectors'; } from '../../redux/selectors';
import { UserFetchState, User } from '../../redux/prop-types'; import { UserFetchState, User } from '../../redux/prop-types';
@@ -56,6 +58,7 @@ import './fonts.css';
import './global.css'; import './global.css';
import './variables.css'; import './variables.css';
import './rtl-layout.css'; import './rtl-layout.css';
import { LocalStorageThemes } from '../../redux/types';
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
isSignedInSelector, isSignedInSelector,
@@ -65,6 +68,7 @@ const mapStateToProps = createSelector(
isServerOnlineSelector, isServerOnlineSelector,
userFetchStateSelector, userFetchStateSelector,
userSelector, userSelector,
themeSelector,
( (
isSignedIn, isSignedIn,
examInProgress: boolean, examInProgress: boolean,
@@ -72,7 +76,8 @@ const mapStateToProps = createSelector(
isOnline: boolean, isOnline: boolean,
isServerOnline: boolean, isServerOnline: boolean,
fetchState: UserFetchState, fetchState: UserFetchState,
user: User user: User,
theme: LocalStorageThemes
) => ({ ) => ({
isSignedIn, isSignedIn,
examInProgress, examInProgress,
@@ -81,8 +86,8 @@ const mapStateToProps = createSelector(
isOnline, isOnline,
isServerOnline, isServerOnline,
fetchState, fetchState,
theme: user.theme, user,
user theme
}) })
); );
@@ -94,7 +99,8 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
fetchUser, fetchUser,
removeFlashMessage, removeFlashMessage,
onlineStatusChange, onlineStatusChange,
serverStatusChange serverStatusChange,
initializeTheme
}, },
dispatch dispatch
); );
@@ -112,13 +118,6 @@ interface DefaultLayoutProps extends StateProps, DispatchProps {
superBlock?: string; superBlock?: string;
} }
const getSystemTheme = () =>
`${
window.matchMedia('(prefers-color-scheme: dark)').matches === true
? 'dark-palette'
: 'light-palette'
}`;
function DefaultLayout({ function DefaultLayout({
children, children,
hasMessage, hasMessage,
@@ -137,7 +136,8 @@ function DefaultLayout({
theme, theme,
user, user,
pathname, pathname,
fetchUser fetchUser,
initializeTheme
}: DefaultLayoutProps): JSX.Element { }: DefaultLayoutProps): JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
const isMobileLayout = useMediaQuery({ maxWidth: MAX_MOBILE_WIDTH }); const isMobileLayout = useMediaQuery({ maxWidth: MAX_MOBILE_WIDTH });
@@ -148,6 +148,12 @@ function DefaultLayout({
const isExSmallViewportHeight = useMediaQuery({ const isExSmallViewportHeight = useMediaQuery({
maxHeight: EX_SMALL_VIEWPORT_HEIGHT maxHeight: EX_SMALL_VIEWPORT_HEIGHT
}); });
useEffect(() => {
initializeTheme();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => { useEffect(() => {
// componentDidMount // componentDidMount
if (!isSignedIn) { if (!isSignedIn) {
@@ -170,8 +176,6 @@ function DefaultLayout({
return typeof isOnline === 'boolean' ? onlineStatusChange(isOnline) : null; return typeof isOnline === 'boolean' ? onlineStatusChange(isOnline) : null;
}; };
const useSystemTheme = fetchState.complete && isSignedIn === false;
const isJapanese = clientLocale === 'japanese'; const isJapanese = clientLocale === 'japanese';
if (fetchState.pending) { if (fetchState.pending) {
@@ -183,9 +187,7 @@ function DefaultLayout({
envData.environment === 'production' && <StagingWarningModal />} envData.environment === 'production' && <StagingWarningModal />}
<Helmet <Helmet
bodyAttributes={{ bodyAttributes={{
class: useSystemTheme class: `${theme}-palette`
? getSystemTheme()
: `${String(theme) === 'night' ? 'dark' : 'light'}-palette`
}} }}
meta={[ meta={[
{ {

View File

@@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { createStore } from 'redux'; import { createStore } from 'redux';
import { Themes } from '../settings/theme'; import { UserThemes } from '../../redux/types';
import Profile from './profile'; import Profile from './profile';
jest.mock('../../analytics'); jest.mock('../../analytics');
@@ -46,7 +46,7 @@ const userProps = {
sendQuincyEmail: true, sendQuincyEmail: true,
sound: true, sound: true,
keyboardShortcuts: false, keyboardShortcuts: false,
theme: Themes.Default, theme: UserThemes.Default,
twitter: 'string', twitter: 'string',
username: 'string', username: 'string',
website: 'string', website: 'string',

View File

@@ -2,30 +2,26 @@ import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Spacer } from '@freecodecamp/ui'; import { Button, Spacer } from '@freecodecamp/ui';
import { FullWidthRow } from '../helpers'; import { FullWidthRow } from '../helpers';
import ThemeSettings, { ThemeProps } from '../../components/settings/theme';
import SoundSettings from '../../components/settings/sound'; import SoundSettings from '../../components/settings/sound';
import KeyboardShortcutsSettings from '../../components/settings/keyboard-shortcuts'; import KeyboardShortcutsSettings from '../../components/settings/keyboard-shortcuts';
import ScrollbarWidthSettings from '../../components/settings/scrollbar-width'; import ScrollbarWidthSettings from '../../components/settings/scrollbar-width';
type MiscSettingsProps = ThemeProps & { type MiscSettingsProps = {
currentTheme: string;
keyboardShortcuts: boolean; keyboardShortcuts: boolean;
sound: boolean; sound: boolean;
editorLayout: boolean | null; editorLayout: boolean | null;
toggleKeyboardShortcuts: (keyboardShortcuts: boolean) => void; toggleKeyboardShortcuts: (keyboardShortcuts: boolean) => void;
toggleNightMode: () => void;
toggleSoundMode: (sound: boolean) => void; toggleSoundMode: (sound: boolean) => void;
resetEditorLayout: () => void; resetEditorLayout: () => void;
}; };
const MiscSettings = ({ const MiscSettings = ({
currentTheme,
keyboardShortcuts, keyboardShortcuts,
sound, sound,
editorLayout, editorLayout,
resetEditorLayout, resetEditorLayout,
toggleKeyboardShortcuts, toggleKeyboardShortcuts,
toggleNightMode,
toggleSoundMode toggleSoundMode
}: MiscSettingsProps) => { }: MiscSettingsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -34,10 +30,6 @@ const MiscSettings = ({
<> <>
<Spacer size='m' /> <Spacer size='m' />
<FullWidthRow> <FullWidthRow>
<ThemeSettings
currentTheme={currentTheme}
toggleNightMode={toggleNightMode}
/>
<SoundSettings sound={sound} toggleSoundMode={toggleSoundMode} /> <SoundSettings sound={sound} toggleSoundMode={toggleSoundMode} />
<KeyboardShortcutsSettings <KeyboardShortcutsSettings
keyboardShortcuts={keyboardShortcuts} keyboardShortcuts={keyboardShortcuts}

View File

@@ -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';

View File

@@ -4,6 +4,9 @@ export const ns = 'app';
export const actionTypes = createTypes( export const actionTypes = createTypes(
[ [
'setTheme',
'initializeTheme',
'toggleTheme',
'appMount', 'appMount',
'hardGoTo', 'hardGoTo',
'allowBlockDonationRequests', 'allowBlockDonationRequests',

View File

@@ -52,6 +52,10 @@ export const fetchUser = createAction(actionTypes.fetchUser);
export const fetchUserComplete = createAction(actionTypes.fetchUserComplete); export const fetchUserComplete = createAction(actionTypes.fetchUserComplete);
export const fetchUserError = createAction(actionTypes.fetchUserError); 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( export const updateAllChallengesInfo = createAction(
actionTypes.updateAllChallengesInfo actionTypes.updateAllChallengesInfo
); );

View File

@@ -23,6 +23,7 @@ import { createUserTokenSaga } from './user-token-saga';
import { createMsUsernameSaga } from './ms-username-saga'; import { createMsUsernameSaga } from './ms-username-saga';
import { createSurveySaga } from './survey-saga'; import { createSurveySaga } from './survey-saga';
import { createSessionCompletedChallengesSaga } from './session-completed-challenges'; import { createSessionCompletedChallengesSaga } from './session-completed-challenges';
import { createThemeSaga } from './theme-saga';
const defaultFetchState = { const defaultFetchState = {
pending: true, pending: true,
@@ -55,6 +56,7 @@ const initialState = {
currentChallengeId: store.get(CURRENT_CHALLENGE_KEY), currentChallengeId: store.get(CURRENT_CHALLENGE_KEY),
examInProgress: false, examInProgress: false,
isProcessing: false, isProcessing: false,
theme: 'light',
showCert: {}, showCert: {},
showCertFetchState: { showCertFetchState: {
...defaultFetchState ...defaultFetchState
@@ -87,6 +89,7 @@ export const epics = [hardGoToEpic, failedUpdatesEpic, updateCompleteEpic];
export const sagas = [ export const sagas = [
...createAcceptTermsSaga(actionTypes), ...createAcceptTermsSaga(actionTypes),
...createThemeSaga(actionTypes),
...createAppMountSaga(actionTypes), ...createAppMountSaga(actionTypes),
...createDonationSaga(actionTypes), ...createDonationSaga(actionTypes),
...createFetchUserSaga(actionTypes), ...createFetchUserSaga(actionTypes),
@@ -253,6 +256,10 @@ export const reducer = handleActions(
error: payload error: payload
} }
}), }),
[actionTypes.setTheme]: (state, { payload: theme }) => ({
...state,
theme
}),
[actionTypes.onlineStatusChange]: (state, { payload: isOnline }) => ({ [actionTypes.onlineStatusChange]: (state, { payload: isOnline }) => ({
...state, ...state,
isOnline isOnline
@@ -481,8 +488,6 @@ export const reducer = handleActions(
payload ? spreadThePayloadOnUser(state, payload) : state, payload ? spreadThePayloadOnUser(state, payload) : state,
[settingsTypes.updateMySoundComplete]: (state, { payload }) => [settingsTypes.updateMySoundComplete]: (state, { payload }) =>
payload ? spreadThePayloadOnUser(state, payload) : state, payload ? spreadThePayloadOnUser(state, payload) : state,
[settingsTypes.updateMyThemeComplete]: (state, { payload }) =>
payload ? spreadThePayloadOnUser(state, payload) : state,
[settingsTypes.updateMyKeyboardShortcutsComplete]: (state, { payload }) => [settingsTypes.updateMyKeyboardShortcutsComplete]: (state, { payload }) =>
payload ? spreadThePayloadOnUser(state, payload) : state, payload ? spreadThePayloadOnUser(state, payload) : state,
[settingsTypes.updateMyHonestyComplete]: (state, { payload }) => [settingsTypes.updateMyHonestyComplete]: (state, { payload }) =>

View File

@@ -2,8 +2,8 @@ import { HandlerProps } from 'react-reflex';
import { SuperBlocks } from '../../../shared/config/curriculum'; import { SuperBlocks } from '../../../shared/config/curriculum';
import { BlockLayouts, BlockTypes } from '../../../shared/config/blocks'; import { BlockLayouts, BlockTypes } from '../../../shared/config/blocks';
import type { ChallengeFile, Ext } from '../../../shared/utils/polyvinyl'; import type { ChallengeFile, Ext } from '../../../shared/utils/polyvinyl';
import { Themes } from '../components/settings/theme';
import { type CertTitle } from '../../config/cert-and-project-map'; import { type CertTitle } from '../../config/cert-and-project-map';
import { UserThemes } from './types';
export type { ChallengeFile, Ext }; export type { ChallengeFile, Ext };
@@ -316,7 +316,7 @@ export type User = {
savedChallenges: SavedChallenges; savedChallenges: SavedChallenges;
sendQuincyEmail: boolean; sendQuincyEmail: boolean;
sound: boolean; sound: boolean;
theme: Themes; theme: UserThemes;
keyboardShortcuts: boolean; keyboardShortcuts: boolean;
twitter: string; twitter: string;
username: string; username: string;

View File

@@ -259,6 +259,10 @@ export const allChallengesInfoSelector = state =>
export const userProfileFetchStateSelector = state => export const userProfileFetchStateSelector = state =>
state[MainApp].userProfileFetchState; state[MainApp].userProfileFetchState;
export const usernameSelector = state => state[MainApp].appUsername; 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 => { export const userSelector = state => {
const username = usernameSelector(state); const username = usernameSelector(state);

View File

@@ -10,7 +10,6 @@ export const actionTypes = createTypes(
...createAsyncTypes('updateMyEmail'), ...createAsyncTypes('updateMyEmail'),
...createAsyncTypes('updateMySocials'), ...createAsyncTypes('updateMySocials'),
...createAsyncTypes('updateMySound'), ...createAsyncTypes('updateMySound'),
...createAsyncTypes('updateMyTheme'),
...createAsyncTypes('updateMyKeyboardShortcuts'), ...createAsyncTypes('updateMyKeyboardShortcuts'),
...createAsyncTypes('updateMyHonesty'), ...createAsyncTypes('updateMyHonesty'),
...createAsyncTypes('updateMyQuincyEmail'), ...createAsyncTypes('updateMyQuincyEmail'),

View File

@@ -46,13 +46,6 @@ export const updateMySoundComplete = createAction(
); );
export const updateMySoundError = createAction(types.updateMySoundError); 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( export const updateMyKeyboardShortcuts = createAction(
types.updateMyKeyboardShortcuts types.updateMyKeyboardShortcuts
); );

View File

@@ -24,7 +24,6 @@ import {
putUpdateMyProfileUI, putUpdateMyProfileUI,
putUpdateMyQuincyEmail, putUpdateMyQuincyEmail,
putUpdateMySocials, putUpdateMySocials,
putUpdateMyTheme,
putUpdateMyUsername, putUpdateMyUsername,
putVerifyCert putVerifyCert
} from '../../utils/ajax'; } from '../../utils/ajax';
@@ -50,8 +49,6 @@ import {
updateMySocialsError, updateMySocialsError,
updateMySoundComplete, updateMySoundComplete,
updateMySoundError, updateMySoundError,
updateMyThemeComplete,
updateMyThemeError,
validateUsernameComplete, validateUsernameComplete,
validateUsernameError, validateUsernameError,
verifyCertComplete, 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 }) { function* updateMyKeyboardShortcutsSaga({ payload: update }) {
try { try {
const { data } = yield call(putUpdateMyKeyboardShortcuts, update); const { data } = yield call(putUpdateMyKeyboardShortcuts, update);
@@ -248,7 +235,6 @@ export function createSettingsSagas(types) {
takeEvery(types.updateMyHonesty, updateMyHonestySaga), takeEvery(types.updateMyHonesty, updateMyHonestySaga),
takeEvery(types.updateMySound, updateMySoundSaga), takeEvery(types.updateMySound, updateMySoundSaga),
takeEvery(types.resetMyEditorLayout, resetMyEditorLayoutSaga), takeEvery(types.resetMyEditorLayout, resetMyEditorLayoutSaga),
takeEvery(types.updateMyTheme, updateMyThemeSaga),
takeEvery(types.updateMyKeyboardShortcuts, updateMyKeyboardShortcutsSaga), takeEvery(types.updateMyKeyboardShortcuts, updateMyKeyboardShortcutsSaga),
takeEvery(types.updateMyQuincyEmail, updateMyQuincyEmailSaga), takeEvery(types.updateMyQuincyEmail, updateMyQuincyEmailSaga),
takeEvery(types.updateMyPortfolio, updateMyPortfolioSaga), takeEvery(types.updateMyPortfolio, updateMyPortfolioSaga),

View 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)
];
}

View File

@@ -62,3 +62,13 @@ export interface UpdateCardState {
success: boolean; success: boolean;
error: string; error: string;
} }
export enum LocalStorageThemes {
Light = 'light',
Dark = 'dark'
}
export enum UserThemes {
Night = 'night',
Default = 'default'
}

View File

@@ -18,12 +18,12 @@ import store from 'store';
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Loader } from '../../../components/helpers'; import { Loader } from '../../../components/helpers';
import { Themes } from '../../../components/settings/theme'; import { LocalStorageThemes } from '../../../redux/types';
import { saveChallenge } from '../../../redux/actions'; import { saveChallenge } from '../../../redux/actions';
import { import {
isDonationModalOpenSelector, isDonationModalOpenSelector,
isSignedInSelector, isSignedInSelector,
userSelector themeSelector
} from '../../../redux/selectors'; } from '../../../redux/selectors';
import { import {
ChallengeFiles, ChallengeFiles,
@@ -101,7 +101,7 @@ export interface EditorProps {
stopResetting: () => void; stopResetting: () => void;
resetAttempts: () => void; resetAttempts: () => void;
tests: Test[]; tests: Test[];
theme: Themes; theme: LocalStorageThemes;
title: string; title: string;
showProjectPreview: boolean; showProjectPreview: boolean;
previewOpen: boolean; previewOpen: boolean;
@@ -137,9 +137,9 @@ const mapStateToProps = createSelector(
isProjectPreviewModalOpenSelector, isProjectPreviewModalOpenSelector,
isResettingSelector, isResettingSelector,
isSignedInSelector, isSignedInSelector,
userSelector,
challengeTestsSelector, challengeTestsSelector,
isChallengeCompletedSelector, isChallengeCompletedSelector,
themeSelector,
( (
attempts: number, attempts: number,
canFocus: boolean, canFocus: boolean,
@@ -148,9 +148,9 @@ const mapStateToProps = createSelector(
previewOpen: boolean, previewOpen: boolean,
isResetting: boolean, isResetting: boolean,
isSignedIn: boolean, isSignedIn: boolean,
{ theme }: { theme: Themes },
tests: [{ text: string; testString: string; message?: string }], tests: [{ text: string; testString: string; message?: string }],
isChallengeCompleted: boolean isChallengeCompleted: boolean,
theme: LocalStorageThemes
) => ({ ) => ({
attempts, attempts,
canFocus: open ? false : canFocus, canFocus: open ? false : canFocus,
@@ -158,9 +158,9 @@ const mapStateToProps = createSelector(
previewOpen, previewOpen,
isResetting, isResetting,
isSignedIn, isSignedIn,
theme,
tests, tests,
isChallengeCompleted isChallengeCompleted,
theme
}) })
); );
@@ -1269,9 +1269,9 @@ const Editor = (props: EditorProps): JSX.Element => {
).matches; ).matches;
const editorSystemTheme = preferDarkScheme ? 'vs-dark-custom' : 'vs-custom'; const editorSystemTheme = preferDarkScheme ? 'vs-dark-custom' : 'vs-custom';
const editorTheme = const editorTheme =
theme === Themes.Night theme === LocalStorageThemes.Dark
? 'vs-dark-custom' ? 'vs-dark-custom'
: theme === Themes.Default : theme === LocalStorageThemes.Light
? 'vs-custom' ? 'vs-custom'
: editorSystemTheme; : editorSystemTheme;

View File

@@ -2,10 +2,7 @@ import React, { useRef } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex'; import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { import { isDonationModalOpenSelector } from '../../../redux/selectors';
userSelector,
isDonationModalOpenSelector
} from '../../../redux/selectors';
import { import {
canFocusEditorSelector, canFocusEditorSelector,
consoleOutputSelector, consoleOutputSelector,
@@ -14,7 +11,6 @@ import {
import { getTargetEditor } from '../utils/get-target-editor'; import { getTargetEditor } from '../utils/get-target-editor';
import './editor.css'; import './editor.css';
import { FileKey } from '../../../redux/prop-types'; import { FileKey } from '../../../redux/prop-types';
import { Themes } from '../../../components/settings/theme';
import Editor, { type EditorProps } from './editor'; import Editor, { type EditorProps } from './editor';
export type VisibleEditors = { export type VisibleEditors = {
@@ -51,18 +47,15 @@ const mapStateToProps = createSelector(
canFocusEditorSelector, canFocusEditorSelector,
consoleOutputSelector, consoleOutputSelector,
isDonationModalOpenSelector, isDonationModalOpenSelector,
userSelector,
( (
visibleEditors: VisibleEditors, visibleEditors: VisibleEditors,
canFocus: boolean, canFocus: boolean,
output: string[], output: string[],
open, open
{ theme }: { theme: Themes }
) => ({ ) => ({
visibleEditors, visibleEditors,
canFocus: open ? false : canFocus, canFocus: open ? false : canFocus,
output, output
theme
}) })
); );

View File

@@ -363,12 +363,6 @@ export function putUpdateMySocials(
return put('/update-my-socials', update); 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( export function putUpdateMyKeyboardShortcuts(
update: Record<string, string> update: Record<string, string>
): Promise<ResponseWithData<void>> { ): Promise<ResponseWithData<void>> {

View File

@@ -1,13 +1,12 @@
import store from 'store'; import store from 'store';
import { FlashMessages } from '../../components/Flash/redux/flash-messages'; 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 TRY_AGAIN = 'https://campfire-mode.freecodecamp.org/try-again.mp3';
const CHAL_COMP = 'https://campfire-mode.freecodecamp.org/chal-comp.mp3'; const CHAL_COMP = 'https://campfire-mode.freecodecamp.org/chal-comp.mp3';
const toneUrls = { const toneUrls = {
[Themes.Default]: 'https://campfire-mode.freecodecamp.org/day.mp3', [LocalStorageThemes.Light]: 'https://campfire-mode.freecodecamp.org/day.mp3',
[Themes.Night]: 'https://campfire-mode.freecodecamp.org/night.mp3', [LocalStorageThemes.Dark]: 'https://campfire-mode.freecodecamp.org/night.mp3',
donation: 'https://campfire-mode.freecodecamp.org/donate.mp3', donation: 'https://campfire-mode.freecodecamp.org/donate.mp3',
'tests-completed': CHAL_COMP, 'tests-completed': CHAL_COMP,
'block-toggle': 'https://tonejs.github.io/audio/berklee/guitar_chord1.mp3', 'block-toggle': 'https://tonejs.github.io/audio/berklee/guitar_chord1.mp3',

View File

@@ -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.use({ storageState: { cookies: [], origins: [] } });
test('should be in dark mode', async ({ page }) => { 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/); 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', () => { 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.use({ storageState: { cookies: [], origins: [] } });
test('should be in light mode', async ({ page }) => { 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"); const editor = page.locator("div[role='code'].monaco-editor");
await expect(editor).toHaveClass(/vs(?!\w)/); 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)/);
});
});
}); });
}); });

View File

@@ -168,15 +168,6 @@ test.describe('Settings - Certified User', () => {
name: translations.buttons['download-data'] name: translations.buttons['download-data']
}); });
await expect(downloadButton).toBeVisible(); 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.locator('#legendsound')).toBeVisible();
await expect( await expect(
page.getByText(translations.settings['sound-volume']) page.getByText(translations.settings['sound-volume'])