From 23e241bbc06c615b024b9b79b5e90d919e190890 Mon Sep 17 00:00:00 2001 From: Shaun Hamilton Date: Wed, 1 Dec 2021 18:45:17 +0000 Subject: [PATCH] chore(client): tidy Flash by tidying tone (#44320) * chore(client): tidy Flash by tidying tone * add store import * fix enum, remove default switch case Co-authored-by: Nicholas Carrigan * typo galore Co-authored-by: Nicholas Carrigan (he/him) * rejig everything, because refactoring is fun :upside_down_face: * refactor: DRY playTone * fix url to foss library Co-authored-by: Nicholas Carrigan * alphabetasize FlashMessage enum * add all FlashMessages to tone/index.ts * remove redundant type * my code is correctnpm run develop:client * fix: remove circular dependency * fix: typo * remove logs, play special tones for nightmode * play sound on challengeComplete Co-authored-by: Nicholas Carrigan (he/him) Co-authored-by: Nicholas Carrigan Co-authored-by: Oliver Eyton-Williams --- .../client-only-routes/show-certification.tsx | 3 +- .../src/client-only-routes/show-settings.tsx | 5 +- .../src/components/Donation/donate-form.tsx | 7 +- .../components/Donation/donation-modal.tsx | 13 +--- .../Donation/paypal-button.test.tsx | 3 +- .../src/components/Donation/paypal-button.tsx | 5 +- .../components/Donation/stripe-card-form.tsx | 5 +- .../src/components/Donation/walletsButton.tsx | 5 +- .../components/Flash/redux/flash-messages.ts | 35 ++++++++++ client/src/components/Flash/redux/index.ts | 47 +++++-------- .../Header/components/nav-links.tsx | 11 ++-- client/src/components/Header/header.test.tsx | 21 +++--- .../src/components/profile/profile.test.tsx | 3 +- .../src/components/settings/Certification.js | 3 +- client/src/components/settings/about.tsx | 6 +- client/src/components/settings/theme.tsx | 42 ++++-------- client/src/redux/prop-types.ts | 3 +- client/src/redux/settings/danger-zone-saga.js | 5 +- client/src/redux/settings/settings-sagas.js | 4 +- client/src/redux/webhook-saga.js | 9 +-- .../templates/Challenges/classic/editor.tsx | 7 +- .../redux/execute-challenge-saga.js | 17 ++--- .../Introduction/components/block.tsx | 17 +---- .../components/cert-challenge.tsx | 3 +- .../src/utils/certificate-missing-message.ts | 4 +- .../src/utils/really-weird-error-message.ts | 4 +- client/src/utils/reported-error-message.ts | 4 +- client/src/utils/standard-error-message.ts | 4 +- client/src/utils/tone/index.ts | 66 +++++++++++++++++++ 29 files changed, 216 insertions(+), 145 deletions(-) create mode 100644 client/src/components/Flash/redux/flash-messages.ts create mode 100644 client/src/utils/tone/index.ts diff --git a/client/src/client-only-routes/show-certification.tsx b/client/src/client-only-routes/show-certification.tsx index fc045d2517..9063cdc964 100644 --- a/client/src/client-only-routes/show-certification.tsx +++ b/client/src/client-only-routes/show-certification.tsx @@ -14,6 +14,7 @@ import DonateForm from '../components/Donation/donate-form'; import { createFlashMessage } from '../components/Flash/redux'; import { Loader, Spacer } from '../components/helpers'; import RedirectHome from '../components/redirect-home'; +import { Themes } from '../components/settings/theme'; import { showCertSelector, showCertFetchStateSelector, @@ -268,7 +269,7 @@ const ShowCertification = (props: ShowCertificationProps): JSX.Element => { diff --git a/client/src/client-only-routes/show-settings.tsx b/client/src/client-only-routes/show-settings.tsx index 335c30cbec..b58f60452b 100644 --- a/client/src/client-only-routes/show-settings.tsx +++ b/client/src/client-only-routes/show-settings.tsx @@ -16,6 +16,7 @@ import Honesty from '../components/settings/honesty'; import Internet from '../components/settings/internet'; import Portfolio from '../components/settings/portfolio'; import Privacy from '../components/settings/privacy'; +import { Themes } from '../components/settings/theme'; import WebhookToken from '../components/settings/webhook-token'; import { signInLoadingSelector, @@ -35,7 +36,7 @@ interface ShowSettingsProps { navigate: (location: string) => void; showLoading: boolean; submitNewAbout: () => void; - toggleNightMode: (theme: string) => void; + toggleNightMode: (theme: Themes) => void; toggleSoundMode: (sound: boolean) => void; updateInternetSettings: () => void; updateIsHonest: () => void; @@ -61,7 +62,7 @@ const mapDispatchToProps = { createFlashMessage, navigate, submitNewAbout, - toggleNightMode: (theme: string) => updateUserFlag({ theme }), + toggleNightMode: (theme: Themes) => updateUserFlag({ theme }), toggleSoundMode: (sound: boolean) => updateUserFlag({ sound }), updateInternetSettings: updateUserFlag, updateIsHonest: updateUserFlag, diff --git a/client/src/components/Donation/donate-form.tsx b/client/src/components/Donation/donate-form.tsx index 00cb9351ff..e41389ca5e 100644 --- a/client/src/components/Donation/donate-form.tsx +++ b/client/src/components/Donation/donate-form.tsx @@ -26,6 +26,7 @@ import { postChargeStripeCard } from '../../redux'; import Spacer from '../helpers/spacer'; +import { Themes } from '../settings/theme'; import DonateCompletion from './donate-completion'; import PatreonButton from './patreon-button'; import type { AddDonationData } from './paypal-button'; @@ -63,7 +64,7 @@ type DonateFormProps = { duration: string; handleAuthentication: HandleAuthentication; }) => void; - defaultTheme?: string; + defaultTheme?: Themes; email: string; handleProcessing: (duration: string, amount: number, action: string) => void; donationFormState: DonateFormState; @@ -74,7 +75,7 @@ type DonateFormProps = { label: string, { usd, hours }?: { usd?: string | number; hours?: string } ) => string; - theme: string; + theme: Themes; updateDonationFormState: (state: AddDonationData) => unknown; }; @@ -87,7 +88,7 @@ const mapStateToProps = createSelector( showLoading: DonateFormProps['showLoading'], isSignedIn: DonateFormProps['isSignedIn'], donationFormState: DonateFormState, - { email, theme }: { email: string; theme: string } + { email, theme }: { email: string; theme: Themes } ) => ({ isSignedIn, showLoading, diff --git a/client/src/components/Donation/donation-modal.tsx b/client/src/components/Donation/donation-modal.tsx index 66c328b620..379c11422a 100644 --- a/client/src/components/Donation/donation-modal.tsx +++ b/client/src/components/Donation/donation-modal.tsx @@ -6,7 +6,6 @@ import { connect } from 'react-redux'; import { goToAnchor } from 'react-scrollable-anchor'; import { bindActionCreators, Dispatch, AnyAction } from 'redux'; import { createSelector } from 'reselect'; -import store from 'store'; import { modalDefaultDonation } from '../../../../config/donation-settings'; import Cup from '../../assets/icons/cup'; import Heart from '../../assets/icons/heart'; @@ -18,6 +17,7 @@ import { executeGA } from '../../redux'; import { isLocationSuperBlock } from '../../utils/path-parsers'; +import { playTone } from '../../utils/tone'; import { Spacer } from '../helpers'; import DonateForm from './donate-form'; @@ -76,16 +76,7 @@ function DonateModal({ useEffect(() => { if (show) { - const playSound = store.get('fcc-sound') as boolean | undefined; - if (playSound) { - void import('tone').then(tone => { - const player = new tone.Player( - 'https://campfire-mode.freecodecamp.org/donate.mp3' - ).toDestination(); - if (tone.context.state !== 'running') void tone.context.resume(); - player.autostart = playSound; - }); - } + void playTone('donation'); executeGA({ type: 'modal', data: '/donation-modal' }); executeGA({ type: 'event', diff --git a/client/src/components/Donation/paypal-button.test.tsx b/client/src/components/Donation/paypal-button.test.tsx index 9f971d4e37..5c60f0ec5d 100644 --- a/client/src/components/Donation/paypal-button.test.tsx +++ b/client/src/components/Donation/paypal-button.test.tsx @@ -1,5 +1,6 @@ import { render } from '@testing-library/react'; import React from 'react'; +import { Themes } from '../settings/theme'; import { PaypalButton } from './paypal-button'; @@ -11,7 +12,7 @@ const commonProps = { onDonationStateChange: () => null, isPaypalLoading: true, t: jest.fn(), - theme: 'night', + theme: Themes.Night, handlePaymentButtonLoad: jest.fn(), isMinimalForm: true }; diff --git a/client/src/components/Donation/paypal-button.tsx b/client/src/components/Donation/paypal-button.tsx index e33306ab6f..3211e4a6a9 100644 --- a/client/src/components/Donation/paypal-button.tsx +++ b/client/src/components/Donation/paypal-button.tsx @@ -13,6 +13,7 @@ import { } from '../../../../config/donation-settings'; import envData from '../../../../config/env.json'; import { signInLoadingSelector, userSelector } from '../../redux'; +import { Themes } from '../settings/theme'; import PayPalButtonScriptLoader from './paypal-button-script-loader'; type PaypalButtonProps = { @@ -41,7 +42,7 @@ type PaypalButtonProps = { skipAddDonation?: boolean; t: (label: string) => string; ref?: Ref; - theme: string; + theme: Themes; isSubscription?: boolean; handlePaymentButtonLoad: (provider: 'stripe' | 'paypal') => void; isMinimalForm: boolean | undefined; @@ -131,7 +132,7 @@ export class PaypalButton extends Component< const { duration, planId, amount } = this.state; const { t, theme, isPaypalLoading, isMinimalForm } = this.props; const isSubscription = duration !== 'onetime'; - const buttonColor = theme === 'night' ? 'white' : 'gold'; + const buttonColor = theme === Themes.Night ? 'white' : 'gold'; if (!paypalClientId) { return null; } diff --git a/client/src/components/Donation/stripe-card-form.tsx b/client/src/components/Donation/stripe-card-form.tsx index 29388d4b80..bf517e7d2a 100644 --- a/client/src/components/Donation/stripe-card-form.tsx +++ b/client/src/components/Donation/stripe-card-form.tsx @@ -16,6 +16,7 @@ import type { import React, { useState } from 'react'; import envData from '../../../../config/env.json'; +import { Themes } from '../settings/theme'; import { AddDonationData } from './paypal-button'; const { stripePublicKey }: { stripePublicKey: string | null } = envData; @@ -32,7 +33,7 @@ interface FormPropTypes { handleAuthentication: HandleAuthentication ) => void; t: (label: string) => string; - theme: string; + theme: Themes; processing: boolean; } @@ -92,7 +93,7 @@ const StripeCardForm = ({ style: { base: { fontSize: '18px', - color: `${theme === 'night' ? '#fff' : '#0a0a23'}`, + color: `${theme === Themes.Night ? '#fff' : '#0a0a23'}`, '::placeholder': { color: `#858591` } diff --git a/client/src/components/Donation/walletsButton.tsx b/client/src/components/Donation/walletsButton.tsx index 49a1655b41..c383174460 100644 --- a/client/src/components/Donation/walletsButton.tsx +++ b/client/src/components/Donation/walletsButton.tsx @@ -7,6 +7,7 @@ import { Stripe, loadStripe } from '@stripe/stripe-js'; import type { Token, PaymentRequest } from '@stripe/stripe-js'; import React, { useState, useEffect } from 'react'; import envData from '../../../../config/env.json'; +import { Themes } from '../settings/theme'; import { AddDonationData } from './paypal-button'; const { stripePublicKey }: { stripePublicKey: string | null } = envData; @@ -14,7 +15,7 @@ const { stripePublicKey }: { stripePublicKey: string | null } = envData; interface WrapperProps { label: string; amount: number; - theme: string; + theme: Themes; postStripeDonation: ( token: Token, payerEmail: string | undefined, @@ -98,7 +99,7 @@ const WalletsButton = ({ style: { paymentRequestButton: { type: 'default', - theme: theme === 'night' ? 'light' : 'dark', + theme: theme === Themes.Night ? 'light' : 'dark', height: '43px' } }, diff --git a/client/src/components/Flash/redux/flash-messages.ts b/client/src/components/Flash/redux/flash-messages.ts new file mode 100644 index 0000000000..01057512c5 --- /dev/null +++ b/client/src/components/Flash/redux/flash-messages.ts @@ -0,0 +1,35 @@ +export enum FlashMessages { + AccountDeleted = 'flash.account-deleted', + AddNameSuccess = 'flash.add-name', + AlreadyClaimed = 'flash.already-claimed', + CertClaimSuccess = 'flash.cert-claim-success', + CertificateMissing = 'flash.certificate-missing', + CertsPrivate = 'flash.certs-private', + CreateTokenErr = 'flash.create-token-err', + DeleteTokenErr = 'flash.delete-token-err', + EmailValid = 'flash.email-valid', + HonestFirst = 'flash.honest-first', + IncompleteSteps = 'flash.incomplete-steps', + NameNeeded = 'flash.name-needed', + None = '', + NotEligible = 'flash.not-eligible', + NotHonest = 'flash.not-honest', + NotRight = 'flash.not-right', + ProfilePrivate = 'flash.profile-private', + ProgressReset = 'flash.progress-reset', + ProvideUsername = 'flash.provide-username', + ReallyWeird = 'flash.really-weird', + ReportSent = 'flash.report-sent', + SigninSuccess = 'flash.signin-success', + TokenCreated = 'flash.token-created', + TokenDeleted = 'flash.token-deleted', + UpdatedPreferences = 'flash.updated-preferences', + UsernameNotFound = 'flash.username-not-found', + UsernameTaken = 'flash.username-taken', + UsernameUpdated = 'flash.username-updated', + UsernameUsed = 'flash.username-used', + UserNotCertified = 'flash.user-not-certified', + WrongName = 'flash.wrong-name', + WrongUpdating = 'flash.wrong-updating', + WentWrong = 'flash.went-wrong' +} diff --git a/client/src/components/Flash/redux/index.ts b/client/src/components/Flash/redux/index.ts index bbb4bbd71c..40ed312360 100644 --- a/client/src/components/Flash/redux/index.ts +++ b/client/src/components/Flash/redux/index.ts @@ -1,17 +1,12 @@ import { nanoid } from 'nanoid'; -import store from 'store'; + import { FlashState, State } from '../../../redux/types'; +import { playTone } from '../../../utils/tone'; +import { Themes } from '../../settings/theme'; +import { FlashMessages } from './flash-messages'; export const FlashApp = 'flash'; -const initialState = { - message: { - id: '', - type: '', - message: '' - } -}; - export const sagas = []; export const flashMessageSelector = (state: State): FlashState['message'] => @@ -26,32 +21,26 @@ enum FlashActionTypes { export type FlashMessageArg = { type: string; - message: string; + message: FlashMessages; variables?: Record; }; +const initialState = { + message: { + id: '', + type: '', + message: FlashMessages.None + } +}; + export const createFlashMessage = ( flash: FlashMessageArg ): ReducerPayload => { - const playSound = store.get('fcc-sound') as boolean | undefined; - if (playSound) { - void import('tone').then(tone => { - if (tone.context.state !== 'running') { - void tone.context.resume(); - } - if (flash.message === 'flash.incomplete-steps') { - const player = new tone.Player( - 'https://campfire-mode.freecodecamp.org/try-again.mp3' - ).toDestination(); - player.autostart = playSound; - } - if (flash.message === 'flash.cert-claim-success') { - const player = new tone.Player( - 'https://campfire-mode.freecodecamp.org/cert.mp3' - ).toDestination(); - player.autostart = playSound; - } - }); + // Nightmode theme has special tones + if (flash.variables?.theme) { + void playTone(flash.variables.theme as Themes); + } else if (flash.message !== FlashMessages.None) { + void playTone(flash.message); } return { type: FlashActionTypes.CreateFlashMessage, diff --git a/client/src/components/Header/components/nav-links.tsx b/client/src/components/Header/components/nav-links.tsx index fc2fee2c5d..8fe5e0d13f 100644 --- a/client/src/components/Header/components/nav-links.tsx +++ b/client/src/components/Header/components/nav-links.tsx @@ -28,6 +28,7 @@ import { hardGoTo as navigate } from '../../../redux'; import { updateUserFlag } from '../../../redux/settings'; import createLanguageRedirect from '../../create-language-redirect'; import { Link } from '../../helpers'; +import { Themes } from '../../settings/theme'; const { clientLocale, radioLocation, apiLocation } = envData; @@ -46,7 +47,7 @@ export interface NavLinksProps { const mapDispatchToProps = { navigate, - toggleNightMode: (theme: unknown) => updateUserFlag({ theme }) + toggleNightMode: (theme: Themes) => updateUserFlag({ theme }) }; export class NavLinks extends Component { @@ -57,8 +58,10 @@ export class NavLinks extends Component { this.handleLanguageChange = this.handleLanguageChange.bind(this); } - toggleTheme(currentTheme = 'default', toggleNightMode: any) { - toggleNightMode(currentTheme === 'night' ? 'default' : 'night'); + toggleTheme(currentTheme = Themes.Default, toggleNightMode: any) { + toggleNightMode( + currentTheme === Themes.Night ? Themes.Default : Themes.Night + ); } handleLanguageChange = ( @@ -175,7 +178,7 @@ export class NavLinks extends Component { {username ? ( <> {t('settings.labels.night-mode')} - {theme === 'night' ? ( + {theme === Themes.Night ? ( ) : ( diff --git a/client/src/components/Header/header.test.tsx b/client/src/components/Header/header.test.tsx index 7954b7b501..e93b545de2 100644 --- a/client/src/components/Header/header.test.tsx +++ b/client/src/components/Header/header.test.tsx @@ -12,6 +12,7 @@ import { availableLangs, langDisplayNames } from '../../../../config/i18n/all-langs'; +import { Themes } from '../settings/theme'; import AuthOrProfile from './components/auth-or-profile'; import { NavLinks } from './components/nav-links'; import { UniversalNav } from './components/universal-nav'; @@ -50,12 +51,12 @@ describe('', () => { user: { isDonating: false, username: null, - theme: 'default' + theme: Themes.Default }, i18n: { language: 'en' }, - toggleNightMode: (theme: string) => theme, + toggleNightMode: (theme: Themes) => theme, t: t }; const utils = ShallowRenderer.createRenderer(); @@ -81,13 +82,13 @@ describe('', () => { user: { isDonating: false, username: 'nhcarrigan', - theme: 'default' + theme: Themes.Default }, i18n: { language: 'en' }, t: t, - toggleNightMode: (theme: string) => theme + toggleNightMode: (theme: Themes) => theme }; const utils = ShallowRenderer.createRenderer(); utils.render(); @@ -111,13 +112,13 @@ describe('', () => { user: { isDonating: true, username: 'moT01', - theme: 'default' + theme: Themes.Default }, i18n: { language: 'en' }, t: t, - toggleNightMode: (theme: string) => theme + toggleNightMode: (theme: Themes) => theme }; const utils = ShallowRenderer.createRenderer(); utils.render(); @@ -143,13 +144,13 @@ describe('', () => { user: { isDonating: true, username: 'moT01', - theme: 'default' + theme: Themes.Default }, i18n: { language: 'en' }, t: t, - toggleNightMode: (theme: string) => theme + toggleNightMode: (theme: Themes) => theme }; const utils = ShallowRenderer.createRenderer(); utils.render(); @@ -169,13 +170,13 @@ describe('', () => { user: { isDonating: true, username: 'moT01', - theme: 'default' + theme: Themes.Default }, i18n: { language: 'en' }, t: t, - toggleNightMode: (theme: string) => theme + toggleNightMode: (theme: Themes) => theme }; const utils = ShallowRenderer.createRenderer(); utils.render(); diff --git a/client/src/components/profile/profile.test.tsx b/client/src/components/profile/profile.test.tsx index 71d4f2eab6..dcf17cb0ef 100644 --- a/client/src/components/profile/profile.test.tsx +++ b/client/src/components/profile/profile.test.tsx @@ -1,5 +1,6 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; +import { Themes } from '../settings/theme'; import Profile from './profile'; @@ -48,7 +49,7 @@ const userProps = { points: 1, sendQuincyEmail: true, sound: true, - theme: 'string', + theme: Themes.Default, twitter: 'string', username: 'string', website: 'string', diff --git a/client/src/components/settings/Certification.js b/client/src/components/settings/Certification.js index 84786b9c05..8a444431e5 100644 --- a/client/src/components/settings/Certification.js +++ b/client/src/components/settings/Certification.js @@ -17,6 +17,7 @@ import { } from '../../resources/cert-and-project-map'; import { maybeUrlRE } from '../../utils'; +import { FlashMessages } from '../Flash/redux/flash-messages'; import ProjectModal from '../SolutionViewer/ProjectModal'; import { FullWidthRow, Spacer } from '../helpers'; @@ -130,7 +131,7 @@ const isCertMapSelector = createSelector( const honestyInfoMessage = { type: 'info', - message: 'flash.honest-first' + message: FlashMessages.HonestFirst }; const initialState = { diff --git a/client/src/components/settings/about.tsx b/client/src/components/settings/about.tsx index af02526c0f..9586894051 100644 --- a/client/src/components/settings/about.tsx +++ b/client/src/components/settings/about.tsx @@ -11,7 +11,7 @@ import { TFunction, withTranslation } from 'react-i18next'; import { FullWidthRow, Spacer } from '../helpers'; import BlockSaveButton from '../helpers/form/block-save-button'; import SoundSettings from './sound'; -import ThemeSettings from './theme'; +import ThemeSettings, { Themes } from './theme'; import UsernameSettings from './username'; type FormValues = { @@ -23,7 +23,7 @@ type FormValues = { type AboutProps = { about: string; - currentTheme: string; + currentTheme: Themes; location: string; name: string; picture: string; @@ -31,7 +31,7 @@ type AboutProps = { sound: boolean; submitNewAbout: (formValues: FormValues) => void; t: TFunction; - toggleNightMode: (theme: string) => void; + toggleNightMode: (theme: Themes) => void; toggleSoundMode: (sound: boolean) => void; username: string; }; diff --git a/client/src/components/settings/theme.tsx b/client/src/components/settings/theme.tsx index 49f8b635ba..877e3e2ec0 100644 --- a/client/src/components/settings/theme.tsx +++ b/client/src/components/settings/theme.tsx @@ -1,13 +1,17 @@ import { Form } from '@freecodecamp/react-bootstrap'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import store from 'store'; import ToggleSetting from './toggle-setting'; +export enum Themes { + Night = 'night', + Default = 'default' +} + type ThemeProps = { - currentTheme: string; - toggleNightMode: (theme: 'default' | 'night') => void; + currentTheme: Themes; + toggleNightMode: (theme: Themes) => void; }; export default function ThemeSettings({ @@ -23,36 +27,14 @@ export default function ThemeSettings({ > { - const playSound = store.get('fcc-sound') as boolean | undefined; - if (playSound) { - const tone = await import('tone'); - const nightToDayPlayer = new tone.Player( - 'https://campfire-mode.freecodecamp.org/day.mp3' - ).toDestination(); - const dayToNightPlayer = new tone.Player( - 'https://campfire-mode.freecodecamp.org/night.mp3' - ).toDestination(); - if (tone.context.state !== 'running') await tone.context.resume(); - if (currentTheme === 'night') { - if (!nightToDayPlayer.loaded) - await nightToDayPlayer.load( - 'https://campfire-mode.freecodecamp.org/day.mp3' - ); - nightToDayPlayer.start(); - } else { - if (!dayToNightPlayer.loaded) - await dayToNightPlayer.load( - 'https://campfire-mode.freecodecamp.org/night.mp3' - ); - dayToNightPlayer.start(); - } - } - toggleNightMode(currentTheme === 'night' ? 'default' : 'night'); + toggleFlag={() => { + toggleNightMode( + currentTheme === Themes.Night ? Themes.Default : Themes.Night + ); }} /> diff --git a/client/src/redux/prop-types.ts b/client/src/redux/prop-types.ts index cf5f8fdf56..e2ca341838 100644 --- a/client/src/redux/prop-types.ts +++ b/client/src/redux/prop-types.ts @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import { HandlerProps } from 'react-reflex'; import { SuperBlocks } from '../../../config/certification-settings'; +import { Themes } from '../components/settings/theme'; import { certMap } from '../resources/cert-and-project-map'; export const UserPropType = PropTypes.shape({ @@ -246,7 +247,7 @@ export type User = { progressTimestamps: Array; sendQuincyEmail: boolean; sound: boolean; - theme: string; + theme: Themes; twitter: string; username: string; website: string; diff --git a/client/src/redux/settings/danger-zone-saga.js b/client/src/redux/settings/danger-zone-saga.js index 1e1f03bc4f..10d41d4d39 100644 --- a/client/src/redux/settings/danger-zone-saga.js +++ b/client/src/redux/settings/danger-zone-saga.js @@ -3,6 +3,7 @@ import { call, put, takeEvery, take } from 'redux-saga/effects'; import { resetUserData, fetchUser } from '../'; import { createFlashMessage } from '../../components/Flash/redux'; +import { FlashMessages } from '../../components/Flash/redux/flash-messages'; import { postResetProgress, postDeleteAccount } from '../../utils/ajax'; import { actionTypes as appTypes } from '../action-types'; import { deleteAccountError, resetProgressError } from './'; @@ -13,7 +14,7 @@ function* deleteAccountSaga() { yield put( createFlashMessage({ type: 'info', - message: 'flash.account-deleted' + message: FlashMessages.AccountDeleted }) ); // remove current user information from application state @@ -30,7 +31,7 @@ function* resetProgressSaga() { yield put( createFlashMessage({ type: 'info', - message: 'flash.progress-reset' + message: FlashMessages.ProgressReset }) ); // refresh current user data in application state diff --git a/client/src/redux/settings/settings-sagas.js b/client/src/redux/settings/settings-sagas.js index 1e24291848..73b5ce5e44 100644 --- a/client/src/redux/settings/settings-sagas.js +++ b/client/src/redux/settings/settings-sagas.js @@ -58,7 +58,9 @@ function* updateUserFlagSaga({ payload: update }) { try { const response = yield call(putUpdateUserFlag, update); yield put(updateUserFlagComplete({ ...response, payload: update })); - yield put(createFlashMessage(response)); + yield put( + createFlashMessage({ ...response, variables: { theme: update.theme } }) + ); } catch (e) { yield put(updateUserFlagError(e)); } diff --git a/client/src/redux/webhook-saga.js b/client/src/redux/webhook-saga.js index 78e9a23466..dc0960650b 100644 --- a/client/src/redux/webhook-saga.js +++ b/client/src/redux/webhook-saga.js @@ -1,24 +1,25 @@ import { call, put, takeEvery } from 'redux-saga/effects'; import { createFlashMessage } from '../components/Flash/redux'; +import { FlashMessages } from '../components/Flash/redux/flash-messages'; import { postWebhookToken, deleteWebhookToken } from '../utils/ajax'; import { postWebhookTokenComplete, deleteWebhookTokenComplete } from '.'; const message = { created: { type: 'success', - message: 'flash.token-created' + message: FlashMessages.TokenCreated }, createErr: { type: 'danger', - message: 'flash.create-token-err' + message: FlashMessages.CreateTokenErr }, deleted: { type: 'info', - message: 'flash.token-deleted' + message: FlashMessages.TokenDeleted }, deleteErr: { type: 'danger', - message: 'flash.delete-token-err' + message: FlashMessages.DeleteTokenErr } }; diff --git a/client/src/templates/Challenges/classic/editor.tsx b/client/src/templates/Challenges/classic/editor.tsx index 8150eb63d8..a1cdac7125 100644 --- a/client/src/templates/Challenges/classic/editor.tsx +++ b/client/src/templates/Challenges/classic/editor.tsx @@ -19,6 +19,7 @@ import { createSelector } from 'reselect'; import store from 'store'; import { Loader } from '../../../components/helpers'; +import { Themes } from '../../../components/settings/theme'; import { userSelector, isDonationModalOpenSelector } from '../../../redux'; import { ChallengeFiles, @@ -73,7 +74,7 @@ interface EditorProps { submitChallenge: () => void; stopResetting: () => void; tests: Test[]; - theme: string; + theme: Themes; title: string; updateFile: (object: { fileKey: FileKey; @@ -111,7 +112,7 @@ const mapStateToProps = createSelector( output: string[], open, isResetting: boolean, - { theme = 'default' }: { theme: string }, + { theme = Themes.Default }: { theme: Themes }, tests: [{ text: string; testString: string }] ) => ({ canFocus: open ? false : canFocus, @@ -994,7 +995,7 @@ const Editor = (props: EditorProps): JSX.Element => { } const { theme } = props; - const editorTheme = theme === 'night' ? 'vs-dark-custom' : 'vs-custom'; + const editorTheme = theme === Themes.Night ? 'vs-dark-custom' : 'vs-custom'; return ( }> diff --git a/client/src/templates/Challenges/redux/execute-challenge-saga.js b/client/src/templates/Challenges/redux/execute-challenge-saga.js index 83217a8fff..4f1bdde20c 100644 --- a/client/src/templates/Challenges/redux/execute-challenge-saga.js +++ b/client/src/templates/Challenges/redux/execute-challenge-saga.js @@ -13,8 +13,8 @@ import { take, cancel } from 'redux-saga/effects'; -import store from 'store'; +import { playTone } from '../../../utils/tone'; import { buildChallenge, canBuildChallenge, @@ -99,17 +99,10 @@ export function* executeChallengeSaga({ payload }) { yield put(updateTests(testResults)); const challengeComplete = testResults.every(test => test.pass && !test.err); - const playSound = store.get('fcc-sound'); - let player; - if (playSound) { - void import('tone').then(tone => { - player = new tone.Player( - challengeComplete && payload?.showCompletionModal - ? 'https://campfire-mode.freecodecamp.org/chal-comp.mp3' - : 'https://campfire-mode.freecodecamp.org/try-again.mp3' - ).toDestination(); - player.autostart = true; - }); + if (challengeComplete) { + playTone('tests-completed'); + } else { + playTone('tests-failed'); } if (challengeComplete && payload?.showCompletionModal) { yield put(openModal('completion')); diff --git a/client/src/templates/Introduction/components/block.tsx b/client/src/templates/Introduction/components/block.tsx index 7d4b255bcf..6d0db6050f 100644 --- a/client/src/templates/Introduction/components/block.tsx +++ b/client/src/templates/Introduction/components/block.tsx @@ -4,7 +4,6 @@ import { connect } from 'react-redux'; import ScrollableAnchor from 'react-scrollable-anchor'; import { bindActionCreators, Dispatch } from 'redux'; import { createSelector } from 'reselect'; -import store from 'store'; import envData from '../../../../../config/env.json'; import { isAuditedCert } from '../../../../../utils/is-audited'; @@ -14,6 +13,7 @@ import GreenPass from '../../../assets/icons/green-pass'; import { Link } from '../../../components/helpers'; import { completedChallengesSelector, executeGA } from '../../../redux'; import { ChallengeNode, CompletedChallenge } from '../../../redux/prop-types'; +import { playTone } from '../../../utils/tone'; import { makeExpandedBlockSelector, toggleBlock } from '../redux'; import Challenges from './Challenges'; @@ -61,20 +61,7 @@ export class Block extends Component { handleBlockClick(): void { const { blockDashedName, toggleBlock, executeGA } = this.props; - const playSound = store.get('fcc-sound') as boolean; - if (playSound) { - void (async () => { - const tone = await import('tone'); - - const player = new tone.Player( - 'https://tonejs.github.io/audio/berklee/guitar_chord1.mp3' - ).toDestination(); - if (tone.context.state !== 'running') { - void tone.context.resume(); - } - player.autostart = playSound; - })(); - } + void playTone('block-toggle'); executeGA({ type: 'event', data: { diff --git a/client/src/templates/Introduction/components/cert-challenge.tsx b/client/src/templates/Introduction/components/cert-challenge.tsx index 81e4327aad..3cfd947466 100644 --- a/client/src/templates/Introduction/components/cert-challenge.tsx +++ b/client/src/templates/Introduction/components/cert-challenge.tsx @@ -10,6 +10,7 @@ import { SuperBlocks } from '../../../../../config/certification-settings'; import { createFlashMessage } from '../../../components/Flash/redux'; +import { FlashMessages } from '../../../components/Flash/redux/flash-messages'; import { userFetchStateSelector, stepsToClaimSelector, @@ -40,7 +41,7 @@ interface CertChallengeProps { const honestyInfoMessage = { type: 'info', - message: 'flash.honest-first' + message: FlashMessages.HonestFirst }; const mapStateToProps = (state: unknown) => { diff --git a/client/src/utils/certificate-missing-message.ts b/client/src/utils/certificate-missing-message.ts index 5e34dcaf4f..0a1cfeeda5 100644 --- a/client/src/utils/certificate-missing-message.ts +++ b/client/src/utils/certificate-missing-message.ts @@ -1,6 +1,8 @@ +import { FlashMessages } from '../components/Flash/redux/flash-messages'; + const certificateMissingErrorMessage = { type: 'danger', - message: 'flash.certificate-missing' + message: FlashMessages.CertificateMissing }; export default certificateMissingErrorMessage; diff --git a/client/src/utils/really-weird-error-message.ts b/client/src/utils/really-weird-error-message.ts index db9e766394..ca5e58db3f 100644 --- a/client/src/utils/really-weird-error-message.ts +++ b/client/src/utils/really-weird-error-message.ts @@ -1,6 +1,8 @@ +import { FlashMessages } from '../components/Flash/redux/flash-messages'; + const reallyWeirdErrorMessage = { type: 'danger', - message: 'flash.really-weird' + message: FlashMessages.ReallyWeird }; export default reallyWeirdErrorMessage; diff --git a/client/src/utils/reported-error-message.ts b/client/src/utils/reported-error-message.ts index a0062f8052..ad8f6fa9f6 100644 --- a/client/src/utils/reported-error-message.ts +++ b/client/src/utils/reported-error-message.ts @@ -1,6 +1,8 @@ +import { FlashMessages } from '../components/Flash/redux/flash-messages'; + const reportedErrorMessage = { type: 'danger', - message: 'flash.not-right' + message: FlashMessages.NotRight }; export default reportedErrorMessage; diff --git a/client/src/utils/standard-error-message.ts b/client/src/utils/standard-error-message.ts index 6816546c50..a5357cd2ac 100644 --- a/client/src/utils/standard-error-message.ts +++ b/client/src/utils/standard-error-message.ts @@ -1,6 +1,8 @@ +import { FlashMessages } from '../components/Flash/redux/flash-messages'; + const standardErrorMessage = { type: 'danger', - message: 'flash.went-wrong' + message: FlashMessages.WentWrong }; export default standardErrorMessage; diff --git a/client/src/utils/tone/index.ts b/client/src/utils/tone/index.ts new file mode 100644 index 0000000000..c3075521ca --- /dev/null +++ b/client/src/utils/tone/index.ts @@ -0,0 +1,66 @@ +import store from 'store'; +import { FlashMessages } from '../../components/Flash/redux/flash-messages'; +import { Themes } from '../../components/settings/theme'; + +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', + donation: 'https://campfire-mode.freecodecamp.org/donate.mp3', + 'tests-completed': CHAL_COMP, + 'block-toggle': 'https://tonejs.github.io/audio/berklee/guitar_chord1.mp3', + 'tests-failed': TRY_AGAIN, + completion: CHAL_COMP, + [FlashMessages.AccountDeleted]: TRY_AGAIN, + [FlashMessages.AddNameSuccess]: CHAL_COMP, + [FlashMessages.AlreadyClaimed]: TRY_AGAIN, + [FlashMessages.CertClaimSuccess]: + 'https://campfire-mode.freecodecamp.org/cert.mp3', + [FlashMessages.CertificateMissing]: TRY_AGAIN, + [FlashMessages.CertsPrivate]: TRY_AGAIN, + [FlashMessages.CreateTokenErr]: TRY_AGAIN, + [FlashMessages.DeleteTokenErr]: TRY_AGAIN, + [FlashMessages.EmailValid]: CHAL_COMP, + [FlashMessages.HonestFirst]: TRY_AGAIN, + [FlashMessages.IncompleteSteps]: TRY_AGAIN, + [FlashMessages.NameNeeded]: TRY_AGAIN, + // [FlashMessages.None]: '', + [FlashMessages.NotEligible]: TRY_AGAIN, + [FlashMessages.NotHonest]: TRY_AGAIN, + [FlashMessages.NotRight]: TRY_AGAIN, + [FlashMessages.ProfilePrivate]: TRY_AGAIN, + [FlashMessages.ProgressReset]: TRY_AGAIN, + [FlashMessages.ProvideUsername]: TRY_AGAIN, + [FlashMessages.ReallyWeird]: TRY_AGAIN, + [FlashMessages.ReportSent]: CHAL_COMP, + [FlashMessages.SigninSuccess]: CHAL_COMP, + [FlashMessages.TokenCreated]: CHAL_COMP, + [FlashMessages.TokenDeleted]: CHAL_COMP, + [FlashMessages.UpdatedPreferences]: CHAL_COMP, + [FlashMessages.UsernameNotFound]: TRY_AGAIN, + [FlashMessages.UsernameTaken]: TRY_AGAIN, + [FlashMessages.UsernameUpdated]: CHAL_COMP, + [FlashMessages.UsernameUsed]: TRY_AGAIN, + [FlashMessages.UserNotCertified]: TRY_AGAIN, + [FlashMessages.WrongName]: TRY_AGAIN, + [FlashMessages.WrongUpdating]: TRY_AGAIN, + [FlashMessages.WentWrong]: TRY_AGAIN +} as const; + +type ToneStates = keyof typeof toneUrls; + +export async function playTone(state: ToneStates): Promise { + const playSound = !!store.get('fcc-sound'); + if (playSound && toneUrls[state]) { + const tone = await import('tone'); + if (tone.context.state !== 'running') { + tone.context.resume().catch(err => { + console.error('Error resuming audio context', err); + }); + } + const player = new tone.Player(toneUrls[state]).toDestination(); + player.autostart = true; + } +}