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 <nhcarrigan@gmail.com>

* typo galore

Co-authored-by: Nicholas Carrigan (he/him) <nhcarrigan@gmail.com>

* rejig everything, because refactoring is fun 🙃

* refactor: DRY playTone

* fix url to foss library

Co-authored-by: Nicholas Carrigan <nhcarrigan@gmail.com>

* 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) <nhcarrigan@gmail.com>

Co-authored-by: Nicholas Carrigan <nhcarrigan@gmail.com>
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Shaun Hamilton
2021-12-01 18:45:17 +00:00
committed by GitHub
parent edf3185b2b
commit 23e241bbc0
29 changed files with 216 additions and 145 deletions

View File

@ -14,6 +14,7 @@ import DonateForm from '../components/Donation/donate-form';
import { createFlashMessage } from '../components/Flash/redux'; import { createFlashMessage } from '../components/Flash/redux';
import { Loader, Spacer } from '../components/helpers'; import { Loader, Spacer } from '../components/helpers';
import RedirectHome from '../components/redirect-home'; import RedirectHome from '../components/redirect-home';
import { Themes } from '../components/settings/theme';
import { import {
showCertSelector, showCertSelector,
showCertFetchStateSelector, showCertFetchStateSelector,
@ -268,7 +269,7 @@ const ShowCertification = (props: ShowCertificationProps): JSX.Element => {
<Row> <Row>
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}> <Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
<DonateForm <DonateForm
defaultTheme='default' defaultTheme={Themes.Default}
handleProcessing={handleProcessing} handleProcessing={handleProcessing}
isMinimalForm={true} isMinimalForm={true}
/> />

View File

@ -16,6 +16,7 @@ import Honesty from '../components/settings/honesty';
import Internet from '../components/settings/internet'; import Internet from '../components/settings/internet';
import Portfolio from '../components/settings/portfolio'; import Portfolio from '../components/settings/portfolio';
import Privacy from '../components/settings/privacy'; import Privacy from '../components/settings/privacy';
import { Themes } from '../components/settings/theme';
import WebhookToken from '../components/settings/webhook-token'; import WebhookToken from '../components/settings/webhook-token';
import { import {
signInLoadingSelector, signInLoadingSelector,
@ -35,7 +36,7 @@ interface ShowSettingsProps {
navigate: (location: string) => void; navigate: (location: string) => void;
showLoading: boolean; showLoading: boolean;
submitNewAbout: () => void; submitNewAbout: () => void;
toggleNightMode: (theme: string) => void; toggleNightMode: (theme: Themes) => void;
toggleSoundMode: (sound: boolean) => void; toggleSoundMode: (sound: boolean) => void;
updateInternetSettings: () => void; updateInternetSettings: () => void;
updateIsHonest: () => void; updateIsHonest: () => void;
@ -61,7 +62,7 @@ const mapDispatchToProps = {
createFlashMessage, createFlashMessage,
navigate, navigate,
submitNewAbout, submitNewAbout,
toggleNightMode: (theme: string) => updateUserFlag({ theme }), toggleNightMode: (theme: Themes) => updateUserFlag({ theme }),
toggleSoundMode: (sound: boolean) => updateUserFlag({ sound }), toggleSoundMode: (sound: boolean) => updateUserFlag({ sound }),
updateInternetSettings: updateUserFlag, updateInternetSettings: updateUserFlag,
updateIsHonest: updateUserFlag, updateIsHonest: updateUserFlag,

View File

@ -26,6 +26,7 @@ import {
postChargeStripeCard postChargeStripeCard
} from '../../redux'; } from '../../redux';
import Spacer from '../helpers/spacer'; import Spacer from '../helpers/spacer';
import { Themes } from '../settings/theme';
import DonateCompletion from './donate-completion'; import DonateCompletion from './donate-completion';
import PatreonButton from './patreon-button'; import PatreonButton from './patreon-button';
import type { AddDonationData } from './paypal-button'; import type { AddDonationData } from './paypal-button';
@ -63,7 +64,7 @@ type DonateFormProps = {
duration: string; duration: string;
handleAuthentication: HandleAuthentication; handleAuthentication: HandleAuthentication;
}) => void; }) => void;
defaultTheme?: string; defaultTheme?: Themes;
email: string; email: string;
handleProcessing: (duration: string, amount: number, action: string) => void; handleProcessing: (duration: string, amount: number, action: string) => void;
donationFormState: DonateFormState; donationFormState: DonateFormState;
@ -74,7 +75,7 @@ type DonateFormProps = {
label: string, label: string,
{ usd, hours }?: { usd?: string | number; hours?: string } { usd, hours }?: { usd?: string | number; hours?: string }
) => string; ) => string;
theme: string; theme: Themes;
updateDonationFormState: (state: AddDonationData) => unknown; updateDonationFormState: (state: AddDonationData) => unknown;
}; };
@ -87,7 +88,7 @@ const mapStateToProps = createSelector(
showLoading: DonateFormProps['showLoading'], showLoading: DonateFormProps['showLoading'],
isSignedIn: DonateFormProps['isSignedIn'], isSignedIn: DonateFormProps['isSignedIn'],
donationFormState: DonateFormState, donationFormState: DonateFormState,
{ email, theme }: { email: string; theme: string } { email, theme }: { email: string; theme: Themes }
) => ({ ) => ({
isSignedIn, isSignedIn,
showLoading, showLoading,

View File

@ -6,7 +6,6 @@ import { connect } from 'react-redux';
import { goToAnchor } from 'react-scrollable-anchor'; import { goToAnchor } from 'react-scrollable-anchor';
import { bindActionCreators, Dispatch, AnyAction } from 'redux'; import { bindActionCreators, Dispatch, AnyAction } from 'redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import store from 'store';
import { modalDefaultDonation } from '../../../../config/donation-settings'; import { modalDefaultDonation } from '../../../../config/donation-settings';
import Cup from '../../assets/icons/cup'; import Cup from '../../assets/icons/cup';
import Heart from '../../assets/icons/heart'; import Heart from '../../assets/icons/heart';
@ -18,6 +17,7 @@ import {
executeGA executeGA
} from '../../redux'; } from '../../redux';
import { isLocationSuperBlock } from '../../utils/path-parsers'; import { isLocationSuperBlock } from '../../utils/path-parsers';
import { playTone } from '../../utils/tone';
import { Spacer } from '../helpers'; import { Spacer } from '../helpers';
import DonateForm from './donate-form'; import DonateForm from './donate-form';
@ -76,16 +76,7 @@ function DonateModal({
useEffect(() => { useEffect(() => {
if (show) { if (show) {
const playSound = store.get('fcc-sound') as boolean | undefined; void playTone('donation');
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;
});
}
executeGA({ type: 'modal', data: '/donation-modal' }); executeGA({ type: 'modal', data: '/donation-modal' });
executeGA({ executeGA({
type: 'event', type: 'event',

View File

@ -1,5 +1,6 @@
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { Themes } from '../settings/theme';
import { PaypalButton } from './paypal-button'; import { PaypalButton } from './paypal-button';
@ -11,7 +12,7 @@ const commonProps = {
onDonationStateChange: () => null, onDonationStateChange: () => null,
isPaypalLoading: true, isPaypalLoading: true,
t: jest.fn(), t: jest.fn(),
theme: 'night', theme: Themes.Night,
handlePaymentButtonLoad: jest.fn(), handlePaymentButtonLoad: jest.fn(),
isMinimalForm: true isMinimalForm: true
}; };

View File

@ -13,6 +13,7 @@ import {
} from '../../../../config/donation-settings'; } from '../../../../config/donation-settings';
import envData from '../../../../config/env.json'; import envData from '../../../../config/env.json';
import { signInLoadingSelector, userSelector } from '../../redux'; import { signInLoadingSelector, userSelector } from '../../redux';
import { Themes } from '../settings/theme';
import PayPalButtonScriptLoader from './paypal-button-script-loader'; import PayPalButtonScriptLoader from './paypal-button-script-loader';
type PaypalButtonProps = { type PaypalButtonProps = {
@ -41,7 +42,7 @@ type PaypalButtonProps = {
skipAddDonation?: boolean; skipAddDonation?: boolean;
t: (label: string) => string; t: (label: string) => string;
ref?: Ref<PaypalButton>; ref?: Ref<PaypalButton>;
theme: string; theme: Themes;
isSubscription?: boolean; isSubscription?: boolean;
handlePaymentButtonLoad: (provider: 'stripe' | 'paypal') => void; handlePaymentButtonLoad: (provider: 'stripe' | 'paypal') => void;
isMinimalForm: boolean | undefined; isMinimalForm: boolean | undefined;
@ -131,7 +132,7 @@ export class PaypalButton extends Component<
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 !== 'onetime'; const isSubscription = duration !== 'onetime';
const buttonColor = theme === 'night' ? 'white' : 'gold'; const buttonColor = theme === Themes.Night ? 'white' : 'gold';
if (!paypalClientId) { if (!paypalClientId) {
return null; return null;
} }

View File

@ -16,6 +16,7 @@ import type {
import React, { useState } from 'react'; import React, { useState } from 'react';
import envData from '../../../../config/env.json'; import envData from '../../../../config/env.json';
import { Themes } from '../settings/theme';
import { AddDonationData } from './paypal-button'; import { AddDonationData } from './paypal-button';
const { stripePublicKey }: { stripePublicKey: string | null } = envData; const { stripePublicKey }: { stripePublicKey: string | null } = envData;
@ -32,7 +33,7 @@ interface FormPropTypes {
handleAuthentication: HandleAuthentication handleAuthentication: HandleAuthentication
) => void; ) => void;
t: (label: string) => string; t: (label: string) => string;
theme: string; theme: Themes;
processing: boolean; processing: boolean;
} }
@ -92,7 +93,7 @@ const StripeCardForm = ({
style: { style: {
base: { base: {
fontSize: '18px', fontSize: '18px',
color: `${theme === 'night' ? '#fff' : '#0a0a23'}`, color: `${theme === Themes.Night ? '#fff' : '#0a0a23'}`,
'::placeholder': { '::placeholder': {
color: `#858591` color: `#858591`
} }

View File

@ -7,6 +7,7 @@ import { Stripe, loadStripe } from '@stripe/stripe-js';
import type { Token, PaymentRequest } from '@stripe/stripe-js'; import type { Token, PaymentRequest } from '@stripe/stripe-js';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import envData from '../../../../config/env.json'; import envData from '../../../../config/env.json';
import { Themes } from '../settings/theme';
import { AddDonationData } from './paypal-button'; import { AddDonationData } from './paypal-button';
const { stripePublicKey }: { stripePublicKey: string | null } = envData; const { stripePublicKey }: { stripePublicKey: string | null } = envData;
@ -14,7 +15,7 @@ const { stripePublicKey }: { stripePublicKey: string | null } = envData;
interface WrapperProps { interface WrapperProps {
label: string; label: string;
amount: number; amount: number;
theme: string; theme: Themes;
postStripeDonation: ( postStripeDonation: (
token: Token, token: Token,
payerEmail: string | undefined, payerEmail: string | undefined,
@ -98,7 +99,7 @@ const WalletsButton = ({
style: { style: {
paymentRequestButton: { paymentRequestButton: {
type: 'default', type: 'default',
theme: theme === 'night' ? 'light' : 'dark', theme: theme === Themes.Night ? 'light' : 'dark',
height: '43px' height: '43px'
} }
}, },

View File

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

View File

@ -1,17 +1,12 @@
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import store from 'store';
import { FlashState, State } from '../../../redux/types'; 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'; export const FlashApp = 'flash';
const initialState = {
message: {
id: '',
type: '',
message: ''
}
};
export const sagas = []; export const sagas = [];
export const flashMessageSelector = (state: State): FlashState['message'] => export const flashMessageSelector = (state: State): FlashState['message'] =>
@ -26,32 +21,26 @@ enum FlashActionTypes {
export type FlashMessageArg = { export type FlashMessageArg = {
type: string; type: string;
message: string; message: FlashMessages;
variables?: Record<string, unknown>; variables?: Record<string, unknown>;
}; };
const initialState = {
message: {
id: '',
type: '',
message: FlashMessages.None
}
};
export const createFlashMessage = ( export const createFlashMessage = (
flash: FlashMessageArg flash: FlashMessageArg
): ReducerPayload<FlashActionTypes.CreateFlashMessage> => { ): ReducerPayload<FlashActionTypes.CreateFlashMessage> => {
const playSound = store.get('fcc-sound') as boolean | undefined; // Nightmode theme has special tones
if (playSound) { if (flash.variables?.theme) {
void import('tone').then(tone => { void playTone(flash.variables.theme as Themes);
if (tone.context.state !== 'running') { } else if (flash.message !== FlashMessages.None) {
void tone.context.resume(); void playTone(flash.message);
}
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;
}
});
} }
return { return {
type: FlashActionTypes.CreateFlashMessage, type: FlashActionTypes.CreateFlashMessage,

View File

@ -28,6 +28,7 @@ import { hardGoTo as navigate } from '../../../redux';
import { updateUserFlag } from '../../../redux/settings'; import { updateUserFlag } from '../../../redux/settings';
import createLanguageRedirect from '../../create-language-redirect'; import createLanguageRedirect from '../../create-language-redirect';
import { Link } from '../../helpers'; import { Link } from '../../helpers';
import { Themes } from '../../settings/theme';
const { clientLocale, radioLocation, apiLocation } = envData; const { clientLocale, radioLocation, apiLocation } = envData;
@ -46,7 +47,7 @@ export interface NavLinksProps {
const mapDispatchToProps = { const mapDispatchToProps = {
navigate, navigate,
toggleNightMode: (theme: unknown) => updateUserFlag({ theme }) toggleNightMode: (theme: Themes) => updateUserFlag({ theme })
}; };
export class NavLinks extends Component<NavLinksProps, {}> { export class NavLinks extends Component<NavLinksProps, {}> {
@ -57,8 +58,10 @@ export class NavLinks extends Component<NavLinksProps, {}> {
this.handleLanguageChange = this.handleLanguageChange.bind(this); this.handleLanguageChange = this.handleLanguageChange.bind(this);
} }
toggleTheme(currentTheme = 'default', toggleNightMode: any) { toggleTheme(currentTheme = Themes.Default, toggleNightMode: any) {
toggleNightMode(currentTheme === 'night' ? 'default' : 'night'); toggleNightMode(
currentTheme === Themes.Night ? Themes.Default : Themes.Night
);
} }
handleLanguageChange = ( handleLanguageChange = (
@ -175,7 +178,7 @@ export class NavLinks extends Component<NavLinksProps, {}> {
{username ? ( {username ? (
<> <>
<span>{t('settings.labels.night-mode')}</span> <span>{t('settings.labels.night-mode')}</span>
{theme === 'night' ? ( {theme === Themes.Night ? (
<FontAwesomeIcon icon={faCheckSquare} /> <FontAwesomeIcon icon={faCheckSquare} />
) : ( ) : (
<FontAwesomeIcon icon={faSquare} /> <FontAwesomeIcon icon={faSquare} />

View File

@ -12,6 +12,7 @@ import {
availableLangs, availableLangs,
langDisplayNames langDisplayNames
} from '../../../../config/i18n/all-langs'; } from '../../../../config/i18n/all-langs';
import { Themes } from '../settings/theme';
import AuthOrProfile from './components/auth-or-profile'; import AuthOrProfile from './components/auth-or-profile';
import { NavLinks } from './components/nav-links'; import { NavLinks } from './components/nav-links';
import { UniversalNav } from './components/universal-nav'; import { UniversalNav } from './components/universal-nav';
@ -50,12 +51,12 @@ describe('<NavLinks />', () => {
user: { user: {
isDonating: false, isDonating: false,
username: null, username: null,
theme: 'default' theme: Themes.Default
}, },
i18n: { i18n: {
language: 'en' language: 'en'
}, },
toggleNightMode: (theme: string) => theme, toggleNightMode: (theme: Themes) => theme,
t: t t: t
}; };
const utils = ShallowRenderer.createRenderer(); const utils = ShallowRenderer.createRenderer();
@ -81,13 +82,13 @@ describe('<NavLinks />', () => {
user: { user: {
isDonating: false, isDonating: false,
username: 'nhcarrigan', username: 'nhcarrigan',
theme: 'default' theme: Themes.Default
}, },
i18n: { i18n: {
language: 'en' language: 'en'
}, },
t: t, t: t,
toggleNightMode: (theme: string) => theme toggleNightMode: (theme: Themes) => theme
}; };
const utils = ShallowRenderer.createRenderer(); const utils = ShallowRenderer.createRenderer();
utils.render(<NavLinks {...landingPageProps} />); utils.render(<NavLinks {...landingPageProps} />);
@ -111,13 +112,13 @@ describe('<NavLinks />', () => {
user: { user: {
isDonating: true, isDonating: true,
username: 'moT01', username: 'moT01',
theme: 'default' theme: Themes.Default
}, },
i18n: { i18n: {
language: 'en' language: 'en'
}, },
t: t, t: t,
toggleNightMode: (theme: string) => theme toggleNightMode: (theme: Themes) => theme
}; };
const utils = ShallowRenderer.createRenderer(); const utils = ShallowRenderer.createRenderer();
utils.render(<NavLinks {...landingPageProps} />); utils.render(<NavLinks {...landingPageProps} />);
@ -143,13 +144,13 @@ describe('<NavLinks />', () => {
user: { user: {
isDonating: true, isDonating: true,
username: 'moT01', username: 'moT01',
theme: 'default' theme: Themes.Default
}, },
i18n: { i18n: {
language: 'en' language: 'en'
}, },
t: t, t: t,
toggleNightMode: (theme: string) => theme toggleNightMode: (theme: Themes) => theme
}; };
const utils = ShallowRenderer.createRenderer(); const utils = ShallowRenderer.createRenderer();
utils.render(<NavLinks {...landingPageProps} />); utils.render(<NavLinks {...landingPageProps} />);
@ -169,13 +170,13 @@ describe('<NavLinks />', () => {
user: { user: {
isDonating: true, isDonating: true,
username: 'moT01', username: 'moT01',
theme: 'default' theme: Themes.Default
}, },
i18n: { i18n: {
language: 'en' language: 'en'
}, },
t: t, t: t,
toggleNightMode: (theme: string) => theme toggleNightMode: (theme: Themes) => theme
}; };
const utils = ShallowRenderer.createRenderer(); const utils = ShallowRenderer.createRenderer();
utils.render(<NavLinks {...landingPageProps} />); utils.render(<NavLinks {...landingPageProps} />);

View File

@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { Themes } from '../settings/theme';
import Profile from './profile'; import Profile from './profile';
@ -48,7 +49,7 @@ const userProps = {
points: 1, points: 1,
sendQuincyEmail: true, sendQuincyEmail: true,
sound: true, sound: true,
theme: 'string', theme: Themes.Default,
twitter: 'string', twitter: 'string',
username: 'string', username: 'string',
website: 'string', website: 'string',

View File

@ -17,6 +17,7 @@ import {
} from '../../resources/cert-and-project-map'; } from '../../resources/cert-and-project-map';
import { maybeUrlRE } from '../../utils'; import { maybeUrlRE } from '../../utils';
import { FlashMessages } from '../Flash/redux/flash-messages';
import ProjectModal from '../SolutionViewer/ProjectModal'; import ProjectModal from '../SolutionViewer/ProjectModal';
import { FullWidthRow, Spacer } from '../helpers'; import { FullWidthRow, Spacer } from '../helpers';
@ -130,7 +131,7 @@ const isCertMapSelector = createSelector(
const honestyInfoMessage = { const honestyInfoMessage = {
type: 'info', type: 'info',
message: 'flash.honest-first' message: FlashMessages.HonestFirst
}; };
const initialState = { const initialState = {

View File

@ -11,7 +11,7 @@ import { TFunction, withTranslation } from 'react-i18next';
import { FullWidthRow, Spacer } from '../helpers'; import { FullWidthRow, Spacer } from '../helpers';
import BlockSaveButton from '../helpers/form/block-save-button'; import BlockSaveButton from '../helpers/form/block-save-button';
import SoundSettings from './sound'; import SoundSettings from './sound';
import ThemeSettings from './theme'; import ThemeSettings, { Themes } from './theme';
import UsernameSettings from './username'; import UsernameSettings from './username';
type FormValues = { type FormValues = {
@ -23,7 +23,7 @@ type FormValues = {
type AboutProps = { type AboutProps = {
about: string; about: string;
currentTheme: string; currentTheme: Themes;
location: string; location: string;
name: string; name: string;
picture: string; picture: string;
@ -31,7 +31,7 @@ type AboutProps = {
sound: boolean; sound: boolean;
submitNewAbout: (formValues: FormValues) => void; submitNewAbout: (formValues: FormValues) => void;
t: TFunction; t: TFunction;
toggleNightMode: (theme: string) => void; toggleNightMode: (theme: Themes) => void;
toggleSoundMode: (sound: boolean) => void; toggleSoundMode: (sound: boolean) => void;
username: string; username: string;
}; };

View File

@ -1,13 +1,17 @@
import { Form } from '@freecodecamp/react-bootstrap'; import { Form } from '@freecodecamp/react-bootstrap';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import store from 'store';
import ToggleSetting from './toggle-setting'; import ToggleSetting from './toggle-setting';
export enum Themes {
Night = 'night',
Default = 'default'
}
type ThemeProps = { type ThemeProps = {
currentTheme: string; currentTheme: Themes;
toggleNightMode: (theme: 'default' | 'night') => void; toggleNightMode: (theme: Themes) => void;
}; };
export default function ThemeSettings({ export default function ThemeSettings({
@ -23,36 +27,14 @@ export default function ThemeSettings({
> >
<ToggleSetting <ToggleSetting
action={t('settings.labels.night-mode')} action={t('settings.labels.night-mode')}
flag={currentTheme === 'night'} flag={currentTheme === Themes.Night}
flagName='currentTheme' flagName='currentTheme'
offLabel={t('buttons.off')} offLabel={t('buttons.off')}
onLabel={t('buttons.on')} onLabel={t('buttons.on')}
toggleFlag={async () => { toggleFlag={() => {
const playSound = store.get('fcc-sound') as boolean | undefined; toggleNightMode(
if (playSound) { currentTheme === Themes.Night ? Themes.Default : Themes.Night
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');
}} }}
/> />
</Form> </Form>

View File

@ -1,6 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { HandlerProps } from 'react-reflex'; import { HandlerProps } from 'react-reflex';
import { SuperBlocks } from '../../../config/certification-settings'; import { SuperBlocks } from '../../../config/certification-settings';
import { Themes } from '../components/settings/theme';
import { certMap } from '../resources/cert-and-project-map'; import { certMap } from '../resources/cert-and-project-map';
export const UserPropType = PropTypes.shape({ export const UserPropType = PropTypes.shape({
@ -246,7 +247,7 @@ export type User = {
progressTimestamps: Array<unknown>; progressTimestamps: Array<unknown>;
sendQuincyEmail: boolean; sendQuincyEmail: boolean;
sound: boolean; sound: boolean;
theme: string; theme: Themes;
twitter: string; twitter: string;
username: string; username: string;
website: string; website: string;

View File

@ -3,6 +3,7 @@ import { call, put, takeEvery, take } from 'redux-saga/effects';
import { resetUserData, fetchUser } from '../'; import { resetUserData, fetchUser } from '../';
import { createFlashMessage } from '../../components/Flash/redux'; import { createFlashMessage } from '../../components/Flash/redux';
import { FlashMessages } from '../../components/Flash/redux/flash-messages';
import { postResetProgress, postDeleteAccount } from '../../utils/ajax'; import { postResetProgress, postDeleteAccount } from '../../utils/ajax';
import { actionTypes as appTypes } from '../action-types'; import { actionTypes as appTypes } from '../action-types';
import { deleteAccountError, resetProgressError } from './'; import { deleteAccountError, resetProgressError } from './';
@ -13,7 +14,7 @@ function* deleteAccountSaga() {
yield put( yield put(
createFlashMessage({ createFlashMessage({
type: 'info', type: 'info',
message: 'flash.account-deleted' message: FlashMessages.AccountDeleted
}) })
); );
// remove current user information from application state // remove current user information from application state
@ -30,7 +31,7 @@ function* resetProgressSaga() {
yield put( yield put(
createFlashMessage({ createFlashMessage({
type: 'info', type: 'info',
message: 'flash.progress-reset' message: FlashMessages.ProgressReset
}) })
); );
// refresh current user data in application state // refresh current user data in application state

View File

@ -58,7 +58,9 @@ function* updateUserFlagSaga({ payload: update }) {
try { try {
const response = yield call(putUpdateUserFlag, update); const response = yield call(putUpdateUserFlag, update);
yield put(updateUserFlagComplete({ ...response, payload: update })); yield put(updateUserFlagComplete({ ...response, payload: update }));
yield put(createFlashMessage(response)); yield put(
createFlashMessage({ ...response, variables: { theme: update.theme } })
);
} catch (e) { } catch (e) {
yield put(updateUserFlagError(e)); yield put(updateUserFlagError(e));
} }

View File

@ -1,24 +1,25 @@
import { call, put, takeEvery } from 'redux-saga/effects'; import { call, put, takeEvery } from 'redux-saga/effects';
import { createFlashMessage } from '../components/Flash/redux'; import { createFlashMessage } from '../components/Flash/redux';
import { FlashMessages } from '../components/Flash/redux/flash-messages';
import { postWebhookToken, deleteWebhookToken } from '../utils/ajax'; import { postWebhookToken, deleteWebhookToken } from '../utils/ajax';
import { postWebhookTokenComplete, deleteWebhookTokenComplete } from '.'; import { postWebhookTokenComplete, deleteWebhookTokenComplete } from '.';
const message = { const message = {
created: { created: {
type: 'success', type: 'success',
message: 'flash.token-created' message: FlashMessages.TokenCreated
}, },
createErr: { createErr: {
type: 'danger', type: 'danger',
message: 'flash.create-token-err' message: FlashMessages.CreateTokenErr
}, },
deleted: { deleted: {
type: 'info', type: 'info',
message: 'flash.token-deleted' message: FlashMessages.TokenDeleted
}, },
deleteErr: { deleteErr: {
type: 'danger', type: 'danger',
message: 'flash.delete-token-err' message: FlashMessages.DeleteTokenErr
} }
}; };

View File

@ -19,6 +19,7 @@ import { createSelector } from 'reselect';
import store from 'store'; import store from 'store';
import { Loader } from '../../../components/helpers'; import { Loader } from '../../../components/helpers';
import { Themes } from '../../../components/settings/theme';
import { userSelector, isDonationModalOpenSelector } from '../../../redux'; import { userSelector, isDonationModalOpenSelector } from '../../../redux';
import { import {
ChallengeFiles, ChallengeFiles,
@ -73,7 +74,7 @@ interface EditorProps {
submitChallenge: () => void; submitChallenge: () => void;
stopResetting: () => void; stopResetting: () => void;
tests: Test[]; tests: Test[];
theme: string; theme: Themes;
title: string; title: string;
updateFile: (object: { updateFile: (object: {
fileKey: FileKey; fileKey: FileKey;
@ -111,7 +112,7 @@ const mapStateToProps = createSelector(
output: string[], output: string[],
open, open,
isResetting: boolean, isResetting: boolean,
{ theme = 'default' }: { theme: string }, { theme = Themes.Default }: { theme: Themes },
tests: [{ text: string; testString: string }] tests: [{ text: string; testString: string }]
) => ({ ) => ({
canFocus: open ? false : canFocus, canFocus: open ? false : canFocus,
@ -994,7 +995,7 @@ const Editor = (props: EditorProps): JSX.Element => {
} }
const { theme } = props; const { theme } = props;
const editorTheme = theme === 'night' ? 'vs-dark-custom' : 'vs-custom'; const editorTheme = theme === Themes.Night ? 'vs-dark-custom' : 'vs-custom';
return ( return (
<Suspense fallback={<Loader timeout={600} />}> <Suspense fallback={<Loader timeout={600} />}>
<span className='notranslate'> <span className='notranslate'>

View File

@ -13,8 +13,8 @@ import {
take, take,
cancel cancel
} from 'redux-saga/effects'; } from 'redux-saga/effects';
import store from 'store';
import { playTone } from '../../../utils/tone';
import { import {
buildChallenge, buildChallenge,
canBuildChallenge, canBuildChallenge,
@ -99,17 +99,10 @@ export function* executeChallengeSaga({ payload }) {
yield put(updateTests(testResults)); yield put(updateTests(testResults));
const challengeComplete = testResults.every(test => test.pass && !test.err); const challengeComplete = testResults.every(test => test.pass && !test.err);
const playSound = store.get('fcc-sound'); if (challengeComplete) {
let player; playTone('tests-completed');
if (playSound) { } else {
void import('tone').then(tone => { playTone('tests-failed');
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 && payload?.showCompletionModal) { if (challengeComplete && payload?.showCompletionModal) {
yield put(openModal('completion')); yield put(openModal('completion'));

View File

@ -4,7 +4,6 @@ import { connect } from 'react-redux';
import ScrollableAnchor from 'react-scrollable-anchor'; import ScrollableAnchor from 'react-scrollable-anchor';
import { bindActionCreators, Dispatch } from 'redux'; import { bindActionCreators, Dispatch } from 'redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import store from 'store';
import envData from '../../../../../config/env.json'; import envData from '../../../../../config/env.json';
import { isAuditedCert } from '../../../../../utils/is-audited'; import { isAuditedCert } from '../../../../../utils/is-audited';
@ -14,6 +13,7 @@ import GreenPass from '../../../assets/icons/green-pass';
import { Link } from '../../../components/helpers'; import { Link } from '../../../components/helpers';
import { completedChallengesSelector, executeGA } from '../../../redux'; import { completedChallengesSelector, executeGA } from '../../../redux';
import { ChallengeNode, CompletedChallenge } from '../../../redux/prop-types'; import { ChallengeNode, CompletedChallenge } from '../../../redux/prop-types';
import { playTone } from '../../../utils/tone';
import { makeExpandedBlockSelector, toggleBlock } from '../redux'; import { makeExpandedBlockSelector, toggleBlock } from '../redux';
import Challenges from './Challenges'; import Challenges from './Challenges';
@ -61,20 +61,7 @@ export class Block extends Component<BlockProps> {
handleBlockClick(): void { handleBlockClick(): void {
const { blockDashedName, toggleBlock, executeGA } = this.props; const { blockDashedName, toggleBlock, executeGA } = this.props;
const playSound = store.get('fcc-sound') as boolean; void playTone('block-toggle');
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;
})();
}
executeGA({ executeGA({
type: 'event', type: 'event',
data: { data: {

View File

@ -10,6 +10,7 @@ import {
SuperBlocks SuperBlocks
} from '../../../../../config/certification-settings'; } from '../../../../../config/certification-settings';
import { createFlashMessage } from '../../../components/Flash/redux'; import { createFlashMessage } from '../../../components/Flash/redux';
import { FlashMessages } from '../../../components/Flash/redux/flash-messages';
import { import {
userFetchStateSelector, userFetchStateSelector,
stepsToClaimSelector, stepsToClaimSelector,
@ -40,7 +41,7 @@ interface CertChallengeProps {
const honestyInfoMessage = { const honestyInfoMessage = {
type: 'info', type: 'info',
message: 'flash.honest-first' message: FlashMessages.HonestFirst
}; };
const mapStateToProps = (state: unknown) => { const mapStateToProps = (state: unknown) => {

View File

@ -1,6 +1,8 @@
import { FlashMessages } from '../components/Flash/redux/flash-messages';
const certificateMissingErrorMessage = { const certificateMissingErrorMessage = {
type: 'danger', type: 'danger',
message: 'flash.certificate-missing' message: FlashMessages.CertificateMissing
}; };
export default certificateMissingErrorMessage; export default certificateMissingErrorMessage;

View File

@ -1,6 +1,8 @@
import { FlashMessages } from '../components/Flash/redux/flash-messages';
const reallyWeirdErrorMessage = { const reallyWeirdErrorMessage = {
type: 'danger', type: 'danger',
message: 'flash.really-weird' message: FlashMessages.ReallyWeird
}; };
export default reallyWeirdErrorMessage; export default reallyWeirdErrorMessage;

View File

@ -1,6 +1,8 @@
import { FlashMessages } from '../components/Flash/redux/flash-messages';
const reportedErrorMessage = { const reportedErrorMessage = {
type: 'danger', type: 'danger',
message: 'flash.not-right' message: FlashMessages.NotRight
}; };
export default reportedErrorMessage; export default reportedErrorMessage;

View File

@ -1,6 +1,8 @@
import { FlashMessages } from '../components/Flash/redux/flash-messages';
const standardErrorMessage = { const standardErrorMessage = {
type: 'danger', type: 'danger',
message: 'flash.went-wrong' message: FlashMessages.WentWrong
}; };
export default standardErrorMessage; export default standardErrorMessage;

View File

@ -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<void> {
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;
}
}