feat: add campfire mode (#42663)
* feat: add campfire mode fix: resolve lint issues feat: add sound to editor fix: restore flash messages fix: linter issues fix: obey sound setting Update the editor to obey the camper's sound setting. chore: apply suggestions from code review Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> fix: use @types/store fix: linter issues feat: simplify sound saga Update client/src/redux/sound-mode-saga.js Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> fix: missing bracket chore: use new s3 bucket fix: lint fix: import only needed bits fix: remove from navbar (was intermittently broken here anyway) fix: dynamic imports? fix: more dynamic imports fix: tweak theme logic chore: boolean | undefined fix: dns fix: no hammer local storage * chore: apply oliver's review suggestions Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * fix: lost an import Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
committed by
GitHub
parent
87c31f5bc9
commit
07bfe87419
@ -251,6 +251,10 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "default"
|
"default": "default"
|
||||||
},
|
},
|
||||||
|
"sound": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
"profileUI": {
|
"profileUI": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"default": {
|
"default": {
|
||||||
|
@ -51,6 +51,7 @@ export const userPropsForSession = [
|
|||||||
'id',
|
'id',
|
||||||
'sendQuincyEmail',
|
'sendQuincyEmail',
|
||||||
'theme',
|
'theme',
|
||||||
|
'sound',
|
||||||
'completedChallengeCount',
|
'completedChallengeCount',
|
||||||
'completedProjectCount',
|
'completedProjectCount',
|
||||||
'completedCertCount',
|
'completedCertCount',
|
||||||
|
@ -133,7 +133,8 @@
|
|||||||
"my-portfolio": "My portfolio",
|
"my-portfolio": "My portfolio",
|
||||||
"my-timeline": "My timeline",
|
"my-timeline": "My timeline",
|
||||||
"my-donations": "My donations",
|
"my-donations": "My donations",
|
||||||
"night-mode": "Night Mode"
|
"night-mode": "Night Mode",
|
||||||
|
"sound-mode": "Campfire Mode"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"certs": "Certifications",
|
"certs": "Certifications",
|
||||||
|
65
client/package-lock.json
generated
65
client/package-lock.json
generated
@ -6264,6 +6264,30 @@
|
|||||||
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
||||||
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
|
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
|
||||||
},
|
},
|
||||||
|
"automation-events": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/automation-events/-/automation-events-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-FVIO4QTVi4u4ORRMszcXhoUj1U8rtgIVCbMKgw1i/zlcbEn1MICbhfquDJiA1QZgh0KysUGuxrn2JeloH04drQ==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.14.6",
|
||||||
|
"tslib": "^2.3.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": {
|
||||||
|
"version": "7.14.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz",
|
||||||
|
"integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==",
|
||||||
|
"requires": {
|
||||||
|
"regenerator-runtime": "^0.13.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"autoprefixer": {
|
"autoprefixer": {
|
||||||
"version": "10.3.7",
|
"version": "10.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.3.7.tgz",
|
||||||
@ -19344,6 +19368,31 @@
|
|||||||
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz",
|
||||||
"integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA=="
|
"integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA=="
|
||||||
},
|
},
|
||||||
|
"standardized-audio-context": {
|
||||||
|
"version": "25.2.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/standardized-audio-context/-/standardized-audio-context-25.2.9.tgz",
|
||||||
|
"integrity": "sha512-8xRl8Cqv3j8UZdRvAYhgqSpVRntjxUBxute+9Zg8zeXTr+aRfDgbAu5FHzTLpUlu8uAJ9232CdOOMkTceSNAWA==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.14.6",
|
||||||
|
"automation-events": "^4.0.3",
|
||||||
|
"tslib": "^2.3.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": {
|
||||||
|
"version": "7.14.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz",
|
||||||
|
"integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==",
|
||||||
|
"requires": {
|
||||||
|
"regenerator-runtime": "^0.13.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"state-toggle": {
|
"state-toggle": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz",
|
||||||
@ -20027,6 +20076,22 @@
|
|||||||
"ieee754": "^1.2.1"
|
"ieee754": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"tone": {
|
||||||
|
"version": "14.7.77",
|
||||||
|
"resolved": "https://registry.npmjs.org/tone/-/tone-14.7.77.tgz",
|
||||||
|
"integrity": "sha512-tCfK73IkLHyzoKUvGq47gyDyxiKLFvKiVCOobynGgBB9Dl0NkxTM2p+eRJXyCYrjJwy9Y0XCMqD3uOYsYt2Fdg==",
|
||||||
|
"requires": {
|
||||||
|
"standardized-audio-context": "^25.1.8",
|
||||||
|
"tslib": "^2.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"totalist": {
|
"totalist": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz",
|
||||||
|
@ -121,6 +121,7 @@
|
|||||||
"sass.js": "0.11.1",
|
"sass.js": "0.11.1",
|
||||||
"store": "2.0.12",
|
"store": "2.0.12",
|
||||||
"stream-browserify": "3.0.0",
|
"stream-browserify": "3.0.0",
|
||||||
|
"tone": "^14.7.77",
|
||||||
"typescript": "4.4.4",
|
"typescript": "4.4.4",
|
||||||
"uuid": "8.3.2",
|
"uuid": "8.3.2",
|
||||||
"validator": "13.6.0"
|
"validator": "13.6.0"
|
||||||
|
@ -35,6 +35,7 @@ interface IShowSettingsProps {
|
|||||||
showLoading: boolean;
|
showLoading: boolean;
|
||||||
submitNewAbout: () => void;
|
submitNewAbout: () => void;
|
||||||
toggleNightMode: (theme: string) => void;
|
toggleNightMode: (theme: string) => void;
|
||||||
|
toggleSoundMode: (sound: boolean) => void;
|
||||||
updateInternetSettings: () => void;
|
updateInternetSettings: () => void;
|
||||||
updateIsHonest: () => void;
|
updateIsHonest: () => void;
|
||||||
updatePortfolio: () => void;
|
updatePortfolio: () => void;
|
||||||
@ -60,6 +61,7 @@ const mapDispatchToProps = {
|
|||||||
navigate,
|
navigate,
|
||||||
submitNewAbout,
|
submitNewAbout,
|
||||||
toggleNightMode: (theme: string) => updateUserFlag({ theme }),
|
toggleNightMode: (theme: string) => updateUserFlag({ theme }),
|
||||||
|
toggleSoundMode: (sound: boolean) => updateUserFlag({ sound }),
|
||||||
updateInternetSettings: updateUserFlag,
|
updateInternetSettings: updateUserFlag,
|
||||||
updateIsHonest: updateUserFlag,
|
updateIsHonest: updateUserFlag,
|
||||||
updatePortfolio: updateUserFlag,
|
updatePortfolio: updateUserFlag,
|
||||||
@ -75,6 +77,7 @@ export function ShowSettings(props: IShowSettingsProps): JSX.Element {
|
|||||||
isSignedIn,
|
isSignedIn,
|
||||||
submitNewAbout,
|
submitNewAbout,
|
||||||
toggleNightMode,
|
toggleNightMode,
|
||||||
|
toggleSoundMode,
|
||||||
user: {
|
user: {
|
||||||
completedChallenges,
|
completedChallenges,
|
||||||
email,
|
email,
|
||||||
@ -101,6 +104,7 @@ export function ShowSettings(props: IShowSettingsProps): JSX.Element {
|
|||||||
picture,
|
picture,
|
||||||
points,
|
points,
|
||||||
theme,
|
theme,
|
||||||
|
sound,
|
||||||
location,
|
location,
|
||||||
name,
|
name,
|
||||||
githubProfile,
|
githubProfile,
|
||||||
@ -143,8 +147,10 @@ export function ShowSettings(props: IShowSettingsProps): JSX.Element {
|
|||||||
name={name}
|
name={name}
|
||||||
picture={picture}
|
picture={picture}
|
||||||
points={points}
|
points={points}
|
||||||
|
sound={sound}
|
||||||
submitNewAbout={submitNewAbout}
|
submitNewAbout={submitNewAbout}
|
||||||
toggleNightMode={toggleNightMode}
|
toggleNightMode={toggleNightMode}
|
||||||
|
toggleSoundMode={toggleSoundMode}
|
||||||
username={username}
|
username={username}
|
||||||
/>
|
/>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
|
@ -6,9 +6,11 @@ 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';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
closeDonationModal,
|
closeDonationModal,
|
||||||
isDonationModalOpenSelector,
|
isDonationModalOpenSelector,
|
||||||
@ -76,6 +78,16 @@ function DonateModal({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (show) {
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
executeGA({ type: 'modal', data: '/donation-modal' });
|
executeGA({ type: 'modal', data: '/donation-modal' });
|
||||||
executeGA({
|
executeGA({
|
||||||
type: 'event',
|
type: 'event',
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
import store from 'store';
|
||||||
import { FlashState, State } from '../../../redux/types';
|
import { FlashState, State } from '../../../redux/types';
|
||||||
|
|
||||||
export const FlashApp = 'flash';
|
export const FlashApp = 'flash';
|
||||||
@ -31,10 +32,32 @@ export type FlashMessageArg = {
|
|||||||
|
|
||||||
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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
type: FlashActionTypes.createFlashMessage,
|
type: FlashActionTypes.createFlashMessage,
|
||||||
payload: { ...flash, id: nanoid() }
|
payload: { ...flash, id: nanoid() }
|
||||||
});
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const removeFlashMessage =
|
export const removeFlashMessage =
|
||||||
(): ReducerPayload<FlashActionTypes.removeFlashMessage> => ({
|
(): ReducerPayload<FlashActionTypes.removeFlashMessage> => ({
|
||||||
|
@ -10,6 +10,7 @@ import React, { Component } from 'react';
|
|||||||
import { TFunction, withTranslation } from 'react-i18next';
|
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 ThemeSettings from './theme';
|
import ThemeSettings from './theme';
|
||||||
import UsernameSettings from './username';
|
import UsernameSettings from './username';
|
||||||
|
|
||||||
@ -27,9 +28,11 @@ type AboutProps = {
|
|||||||
name: string;
|
name: string;
|
||||||
picture: string;
|
picture: string;
|
||||||
points: number;
|
points: number;
|
||||||
|
sound: boolean;
|
||||||
submitNewAbout: (formValues: FormValues) => void;
|
submitNewAbout: (formValues: FormValues) => void;
|
||||||
t: TFunction;
|
t: TFunction;
|
||||||
toggleNightMode: (theme: string) => void;
|
toggleNightMode: (theme: string) => void;
|
||||||
|
toggleSoundMode: (sound: boolean) => void;
|
||||||
username: string;
|
username: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -184,7 +187,14 @@ class AboutSettings extends Component<AboutProps, AboutState> {
|
|||||||
const {
|
const {
|
||||||
formValues: { name, location, picture, about }
|
formValues: { name, location, picture, about }
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const { currentTheme, username, t, toggleNightMode } = this.props;
|
const {
|
||||||
|
currentTheme,
|
||||||
|
sound,
|
||||||
|
username,
|
||||||
|
t,
|
||||||
|
toggleNightMode,
|
||||||
|
toggleSoundMode
|
||||||
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<div className='about-settings'>
|
<div className='about-settings'>
|
||||||
<UsernameSettings username={username} />
|
<UsernameSettings username={username} />
|
||||||
@ -241,6 +251,7 @@ class AboutSettings extends Component<AboutProps, AboutState> {
|
|||||||
currentTheme={currentTheme}
|
currentTheme={currentTheme}
|
||||||
toggleNightMode={toggleNightMode}
|
toggleNightMode={toggleNightMode}
|
||||||
/>
|
/>
|
||||||
|
<SoundSettings sound={sound} toggleSoundMode={toggleSoundMode} />
|
||||||
</FullWidthRow>
|
</FullWidthRow>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
34
client/src/components/settings/sound.tsx
Normal file
34
client/src/components/settings/sound.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Form } from '@freecodecamp/react-bootstrap';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import ToggleSetting from './toggle-setting';
|
||||||
|
|
||||||
|
type SoundProps = {
|
||||||
|
sound: boolean;
|
||||||
|
toggleSoundMode: (sound: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SoundSettings({
|
||||||
|
sound,
|
||||||
|
toggleSoundMode
|
||||||
|
}: SoundProps): JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form inline={true} onSubmit={(e: React.FormEvent) => e.preventDefault()}>
|
||||||
|
<ToggleSetting
|
||||||
|
action={t('settings.labels.sound-mode')}
|
||||||
|
flag={sound}
|
||||||
|
flagName='sound'
|
||||||
|
offLabel={t('buttons.off')}
|
||||||
|
onLabel={t('buttons.on')}
|
||||||
|
toggleFlag={() => {
|
||||||
|
toggleSoundMode(sound ? false : true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SoundSettings.displayName = 'SoundSettings';
|
@ -1,6 +1,7 @@
|
|||||||
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';
|
||||||
|
|
||||||
@ -26,9 +27,33 @@ export default function ThemeSettings({
|
|||||||
flagName='currentTheme'
|
flagName='currentTheme'
|
||||||
offLabel={t('buttons.off')}
|
offLabel={t('buttons.off')}
|
||||||
onLabel={t('buttons.on')}
|
onLabel={t('buttons.on')}
|
||||||
toggleFlag={() =>
|
toggleFlag={async () => {
|
||||||
toggleNightMode(currentTheme === 'night' ? 'default' : 'night')
|
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');
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
@ -14,9 +14,9 @@ import { createGaSaga } from './ga-saga';
|
|||||||
|
|
||||||
import hardGoToEpic from './hard-go-to-epic';
|
import hardGoToEpic from './hard-go-to-epic';
|
||||||
import { createReportUserSaga } from './report-user-saga';
|
import { createReportUserSaga } from './report-user-saga';
|
||||||
|
|
||||||
import { actionTypes as settingsTypes } from './settings/action-types';
|
import { actionTypes as settingsTypes } from './settings/action-types';
|
||||||
import { createShowCertSaga } from './show-cert-saga';
|
import { createShowCertSaga } from './show-cert-saga';
|
||||||
|
import { createSoundModeSaga } from './sound-mode-saga';
|
||||||
import updateCompleteEpic from './update-complete-epic';
|
import updateCompleteEpic from './update-complete-epic';
|
||||||
|
|
||||||
export const MainApp = 'app';
|
export const MainApp = 'app';
|
||||||
@ -74,7 +74,8 @@ export const sagas = [
|
|||||||
...createGaSaga(actionTypes),
|
...createGaSaga(actionTypes),
|
||||||
...createFetchUserSaga(actionTypes),
|
...createFetchUserSaga(actionTypes),
|
||||||
...createShowCertSaga(actionTypes),
|
...createShowCertSaga(actionTypes),
|
||||||
...createReportUserSaga(actionTypes)
|
...createReportUserSaga(actionTypes),
|
||||||
|
...createSoundModeSaga({ ...actionTypes, ...settingsTypes })
|
||||||
];
|
];
|
||||||
|
|
||||||
export const appMount = createAction(actionTypes.appMount);
|
export const appMount = createAction(actionTypes.appMount);
|
||||||
|
@ -118,6 +118,7 @@ export const User = PropTypes.shape({
|
|||||||
})
|
})
|
||||||
),
|
),
|
||||||
sendQuincyEmail: PropTypes.bool,
|
sendQuincyEmail: PropTypes.bool,
|
||||||
|
sound: PropTypes.bool,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
twitter: PropTypes.string,
|
twitter: PropTypes.string,
|
||||||
username: PropTypes.string,
|
username: PropTypes.string,
|
||||||
@ -295,6 +296,7 @@ export type UserType = {
|
|||||||
};
|
};
|
||||||
progressTimestamps: Array<unknown>;
|
progressTimestamps: Array<unknown>;
|
||||||
sendQuincyEmail: boolean;
|
sendQuincyEmail: boolean;
|
||||||
|
sound: boolean;
|
||||||
theme: string;
|
theme: string;
|
||||||
twitter: string;
|
twitter: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
26
client/src/redux/sound-mode-saga.js
Normal file
26
client/src/redux/sound-mode-saga.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/* eslint-disable require-yield */
|
||||||
|
|
||||||
|
import { takeEvery } from 'redux-saga/effects';
|
||||||
|
import store from 'store';
|
||||||
|
|
||||||
|
const soundKey = 'fcc-sound';
|
||||||
|
|
||||||
|
export function setSound(setting) {
|
||||||
|
store.set(soundKey, setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
function* updateLocalSoundSaga({ payload: { user, sound } }) {
|
||||||
|
if (user) {
|
||||||
|
const { sound = false } = user;
|
||||||
|
setSound(sound);
|
||||||
|
} else if (typeof sound !== 'undefined') {
|
||||||
|
setSound(sound);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSoundModeSaga(types) {
|
||||||
|
return [
|
||||||
|
takeEvery(types.fetchUserComplete, updateLocalSoundSaga),
|
||||||
|
takeEvery(types.updateUserFlagComplete, updateLocalSoundSaga)
|
||||||
|
];
|
||||||
|
}
|
@ -17,6 +17,7 @@ import React, {
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
|
|
||||||
import { Loader } from '../../../components/helpers';
|
import { Loader } from '../../../components/helpers';
|
||||||
import { userSelector, isDonationModalOpenSelector } from '../../../redux';
|
import { userSelector, isDonationModalOpenSelector } from '../../../redux';
|
||||||
import {
|
import {
|
||||||
@ -27,7 +28,8 @@ import {
|
|||||||
ResizePropsType,
|
ResizePropsType,
|
||||||
Test
|
Test
|
||||||
} from '../../../redux/prop-types';
|
} from '../../../redux/prop-types';
|
||||||
|
import { editorToneOptions } from '../../../utils/tone/editor-config';
|
||||||
|
import { editorNotes } from '../../../utils/tone/editor-notes';
|
||||||
import {
|
import {
|
||||||
canFocusEditorSelector,
|
canFocusEditorSelector,
|
||||||
consoleOutputSelector,
|
consoleOutputSelector,
|
||||||
@ -44,6 +46,7 @@ import {
|
|||||||
|
|
||||||
import './editor.css';
|
import './editor.css';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
|
||||||
const MonacoEditor = Loadable(() => import('react-monaco-editor'));
|
const MonacoEditor = Loadable(() => import('react-monaco-editor'));
|
||||||
|
|
||||||
interface EditorProps {
|
interface EditorProps {
|
||||||
@ -191,6 +194,17 @@ const Editor = (props: EditorProps): JSX.Element => {
|
|||||||
const monacoRef: MutableRefObject<typeof monacoEditor | null> =
|
const monacoRef: MutableRefObject<typeof monacoEditor | null> =
|
||||||
useRef<typeof monacoEditor>(null);
|
useRef<typeof monacoEditor>(null);
|
||||||
const dataRef = useRef<EditorProperties>({ ...initialData });
|
const dataRef = useRef<EditorProperties>({ ...initialData });
|
||||||
|
const player = useRef<{
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
sampler: any;
|
||||||
|
noteIndex: number;
|
||||||
|
shouldPlay: boolean | undefined;
|
||||||
|
}>({
|
||||||
|
// eslint-disable-next-line no-undefined
|
||||||
|
sampler: undefined,
|
||||||
|
noteIndex: 0,
|
||||||
|
shouldPlay: store.get('fcc-sound') as boolean | undefined
|
||||||
|
});
|
||||||
|
|
||||||
// since editorDidMount runs once with the initial props object, it keeps a
|
// since editorDidMount runs once with the initial props object, it keeps a
|
||||||
// reference to *those* props. If we want it to use the latest props, we can
|
// reference to *those* props. If we want it to use the latest props, we can
|
||||||
@ -265,6 +279,14 @@ const Editor = (props: EditorProps): JSX.Element => {
|
|||||||
);
|
);
|
||||||
dataRef.current.model = model;
|
dataRef.current.model = model;
|
||||||
|
|
||||||
|
if (player.current.shouldPlay && !player.current.sampler) {
|
||||||
|
void import('tone').then(tone => {
|
||||||
|
player.current.sampler = new tone.Sampler(
|
||||||
|
editorToneOptions
|
||||||
|
).toDestination();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: do we need to return this?
|
// TODO: do we need to return this?
|
||||||
return { model };
|
return { model };
|
||||||
};
|
};
|
||||||
@ -535,6 +557,21 @@ const Editor = (props: EditorProps): JSX.Element => {
|
|||||||
coveringRange.startLineNumber - 1,
|
coveringRange.startLineNumber - 1,
|
||||||
coveringRange.endLineNumber + 1
|
coveringRange.endLineNumber + 1
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
if (player.current.sampler?.loaded && player.current.shouldPlay) {
|
||||||
|
void import('tone').then(tone => {
|
||||||
|
if (tone.context.state !== 'running') void tone.context.resume();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
||||||
|
player.current.sampler?.triggerAttack(
|
||||||
|
editorNotes[player.current.noteIndex]
|
||||||
|
);
|
||||||
|
player.current.noteIndex++;
|
||||||
|
if (player.current.noteIndex >= editorNotes.length) {
|
||||||
|
player.current.noteIndex = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
updateFile({ fileKey, editorValue, editableRegionBoundaries });
|
updateFile({ fileKey, editorValue, editableRegionBoundaries });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
take,
|
take,
|
||||||
cancel
|
cancel
|
||||||
} from 'redux-saga/effects';
|
} from 'redux-saga/effects';
|
||||||
|
import store from 'store';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildChallenge,
|
buildChallenge,
|
||||||
@ -97,10 +98,21 @@ 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');
|
||||||
|
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 && payload?.showCompletionModal) {
|
if (challengeComplete && payload?.showCompletionModal) {
|
||||||
yield put(openModal('completion'));
|
yield put(openModal('completion'));
|
||||||
}
|
}
|
||||||
|
|
||||||
yield put(updateConsole(i18next.t('learn.tests-completed')));
|
yield put(updateConsole(i18next.t('learn.tests-completed')));
|
||||||
yield put(logsToConsole(i18next.t('learn.console-output')));
|
yield put(logsToConsole(i18next.t('learn.console-output')));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -5,6 +5,7 @@ import { connect } from 'react-redux';
|
|||||||
import ScrollableAnchor from 'react-scrollable-anchor';
|
import ScrollableAnchor from 'react-scrollable-anchor';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } 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';
|
||||||
@ -56,6 +57,16 @@ export class Block extends Component {
|
|||||||
|
|
||||||
handleBlockClick() {
|
handleBlockClick() {
|
||||||
const { blockDashedName, toggleBlock, executeGA } = this.props;
|
const { blockDashedName, toggleBlock, executeGA } = this.props;
|
||||||
|
const playSound = store.get('fcc-sound');
|
||||||
|
if (playSound) {
|
||||||
|
void import('tone').then(tone => {
|
||||||
|
const player = new tone.Player(
|
||||||
|
'https://tonejs.github.io/audio/berklee/guitar_chord1.mp3'
|
||||||
|
).toDestination();
|
||||||
|
if (tone.context.state !== 'running') tone.context.resume();
|
||||||
|
player.autostart = playSound;
|
||||||
|
});
|
||||||
|
}
|
||||||
executeGA({
|
executeGA({
|
||||||
type: 'event',
|
type: 'event',
|
||||||
data: {
|
data: {
|
||||||
|
51
client/src/utils/tone/editor-config.ts
Normal file
51
client/src/utils/tone/editor-config.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
export const editorToneOptions = {
|
||||||
|
urls: {
|
||||||
|
C1: 'Guitar-Sample-01.mp3',
|
||||||
|
D1: 'Guitar-Sample-02.mp3',
|
||||||
|
E1: 'Guitar-Sample-03.mp3',
|
||||||
|
F1: 'Guitar-Sample-04.mp3',
|
||||||
|
G1: 'Guitar-Sample-05.mp3',
|
||||||
|
A1: 'Guitar-Sample-06.mp3',
|
||||||
|
B1: 'Guitar-Sample-07.mp3',
|
||||||
|
C2: 'Guitar-Sample-08.mp3',
|
||||||
|
D2: 'Guitar-Sample-09.mp3',
|
||||||
|
E2: 'Guitar-Sample-10.mp3',
|
||||||
|
F2: 'Guitar-Sample-11.mp3',
|
||||||
|
G2: 'Guitar-Sample-12.mp3',
|
||||||
|
A2: 'Guitar-Sample-13.mp3',
|
||||||
|
B2: 'Guitar-Sample-14.mp3',
|
||||||
|
C3: 'Guitar-Sample-15.mp3',
|
||||||
|
D3: 'Guitar-Sample-16.mp3',
|
||||||
|
E3: 'Guitar-Sample-17.mp3',
|
||||||
|
F3: 'Guitar-Sample-18.mp3',
|
||||||
|
G3: 'Guitar-Sample-19.mp3',
|
||||||
|
A3: 'Guitar-Sample-20.mp3',
|
||||||
|
B3: 'Guitar-Sample-21.mp3',
|
||||||
|
C4: 'Guitar-Sample-22.mp3',
|
||||||
|
D4: 'Guitar-Sample-23.mp3',
|
||||||
|
E4: 'Guitar-Sample-24.mp3',
|
||||||
|
F4: 'Guitar-Sample-25.mp3',
|
||||||
|
G4: 'Guitar-Sample-26.mp3',
|
||||||
|
A4: 'Guitar-Sample-27.mp3',
|
||||||
|
B4: 'Guitar-Sample-28.mp3',
|
||||||
|
C5: 'Guitar-Sample-29.mp3',
|
||||||
|
D5: 'Guitar-Sample-30.mp3',
|
||||||
|
E5: 'Guitar-Sample-31.mp3',
|
||||||
|
F5: 'Guitar-Sample-32.mp3',
|
||||||
|
G5: 'Guitar-Sample-33.mp3',
|
||||||
|
A5: 'Guitar-Sample-34.mp3',
|
||||||
|
B5: 'Guitar-Sample-35.mp3',
|
||||||
|
C6: 'Guitar-Sample-36.mp3',
|
||||||
|
D6: 'Guitar-Sample-37.mp3',
|
||||||
|
E6: 'Guitar-Sample-38.mp3',
|
||||||
|
F6: 'Guitar-Sample-39.mp3',
|
||||||
|
G6: 'Guitar-Sample-40.mp3',
|
||||||
|
A6: 'Guitar-Sample-41.mp3',
|
||||||
|
B6: 'Guitar-Sample-42.mp3',
|
||||||
|
C7: 'Guitar-Sample-43.mp3',
|
||||||
|
D7: 'Guitar-Sample-44.mp3',
|
||||||
|
E7: 'Guitar-Sample-45.mp3'
|
||||||
|
},
|
||||||
|
release: 1,
|
||||||
|
baseUrl: 'https://campfire-mode.freecodecamp.org/mariachi/'
|
||||||
|
};
|
47
client/src/utils/tone/editor-notes.ts
Normal file
47
client/src/utils/tone/editor-notes.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
export const editorNotes = [
|
||||||
|
'C1',
|
||||||
|
'D1',
|
||||||
|
'E1',
|
||||||
|
'F1',
|
||||||
|
'G1',
|
||||||
|
'A1',
|
||||||
|
'B1',
|
||||||
|
'C2',
|
||||||
|
'D2',
|
||||||
|
'E2',
|
||||||
|
'F2',
|
||||||
|
'G2',
|
||||||
|
'A2',
|
||||||
|
'B2',
|
||||||
|
'C3',
|
||||||
|
'D3',
|
||||||
|
'E3',
|
||||||
|
'F3',
|
||||||
|
'G3',
|
||||||
|
'A3',
|
||||||
|
'B3',
|
||||||
|
'C4',
|
||||||
|
'D4',
|
||||||
|
'E4',
|
||||||
|
'F4',
|
||||||
|
'G4',
|
||||||
|
'A4',
|
||||||
|
'B4',
|
||||||
|
'C5',
|
||||||
|
'D5',
|
||||||
|
'E5',
|
||||||
|
'F5',
|
||||||
|
'G5',
|
||||||
|
'A5',
|
||||||
|
'B5',
|
||||||
|
'C6',
|
||||||
|
'D6',
|
||||||
|
'E6',
|
||||||
|
'F6',
|
||||||
|
'G6',
|
||||||
|
'A6',
|
||||||
|
'B6',
|
||||||
|
'C7',
|
||||||
|
'D7',
|
||||||
|
'E7'
|
||||||
|
];
|
Reference in New Issue
Block a user