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:
@ -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 => {
|
||||
<Row>
|
||||
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<DonateForm
|
||||
defaultTheme='default'
|
||||
defaultTheme={Themes.Default}
|
||||
handleProcessing={handleProcessing}
|
||||
isMinimalForm={true}
|
||||
/>
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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<PaypalButton>;
|
||||
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;
|
||||
}
|
||||
|
@ -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`
|
||||
}
|
||||
|
@ -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'
|
||||
}
|
||||
},
|
||||
|
35
client/src/components/Flash/redux/flash-messages.ts
Normal file
35
client/src/components/Flash/redux/flash-messages.ts
Normal 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'
|
||||
}
|
@ -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<string, unknown>;
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
message: {
|
||||
id: '',
|
||||
type: '',
|
||||
message: FlashMessages.None
|
||||
}
|
||||
};
|
||||
|
||||
export const createFlashMessage = (
|
||||
flash: FlashMessageArg
|
||||
): ReducerPayload<FlashActionTypes.CreateFlashMessage> => {
|
||||
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,
|
||||
|
@ -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<NavLinksProps, {}> {
|
||||
@ -57,8 +58,10 @@ export class NavLinks extends Component<NavLinksProps, {}> {
|
||||
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<NavLinksProps, {}> {
|
||||
{username ? (
|
||||
<>
|
||||
<span>{t('settings.labels.night-mode')}</span>
|
||||
{theme === 'night' ? (
|
||||
{theme === Themes.Night ? (
|
||||
<FontAwesomeIcon icon={faCheckSquare} />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faSquare} />
|
||||
|
@ -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('<NavLinks />', () => {
|
||||
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('<NavLinks />', () => {
|
||||
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(<NavLinks {...landingPageProps} />);
|
||||
@ -111,13 +112,13 @@ describe('<NavLinks />', () => {
|
||||
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(<NavLinks {...landingPageProps} />);
|
||||
@ -143,13 +144,13 @@ describe('<NavLinks />', () => {
|
||||
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(<NavLinks {...landingPageProps} />);
|
||||
@ -169,13 +170,13 @@ describe('<NavLinks />', () => {
|
||||
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(<NavLinks {...landingPageProps} />);
|
||||
|
@ -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',
|
||||
|
@ -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 = {
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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({
|
||||
>
|
||||
<ToggleSetting
|
||||
action={t('settings.labels.night-mode')}
|
||||
flag={currentTheme === 'night'}
|
||||
flag={currentTheme === Themes.Night}
|
||||
flagName='currentTheme'
|
||||
offLabel={t('buttons.off')}
|
||||
onLabel={t('buttons.on')}
|
||||
toggleFlag={async () => {
|
||||
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
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Form>
|
||||
|
@ -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<unknown>;
|
||||
sendQuincyEmail: boolean;
|
||||
sound: boolean;
|
||||
theme: string;
|
||||
theme: Themes;
|
||||
twitter: string;
|
||||
username: string;
|
||||
website: string;
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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 (
|
||||
<Suspense fallback={<Loader timeout={600} />}>
|
||||
<span className='notranslate'>
|
||||
|
@ -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'));
|
||||
|
@ -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<BlockProps> {
|
||||
|
||||
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: {
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
66
client/src/utils/tone/index.ts
Normal file
66
client/src/utils/tone/index.ts
Normal 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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user