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",
|
||||
"default": "default"
|
||||
},
|
||||
"sound": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"profileUI": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
|
@ -51,6 +51,7 @@ export const userPropsForSession = [
|
||||
'id',
|
||||
'sendQuincyEmail',
|
||||
'theme',
|
||||
'sound',
|
||||
'completedChallengeCount',
|
||||
'completedProjectCount',
|
||||
'completedCertCount',
|
||||
|
@ -133,7 +133,8 @@
|
||||
"my-portfolio": "My portfolio",
|
||||
"my-timeline": "My timeline",
|
||||
"my-donations": "My donations",
|
||||
"night-mode": "Night Mode"
|
||||
"night-mode": "Night Mode",
|
||||
"sound-mode": "Campfire Mode"
|
||||
},
|
||||
"headings": {
|
||||
"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",
|
||||
"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": {
|
||||
"version": "10.3.7",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz",
|
||||
@ -20027,6 +20076,22 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz",
|
||||
|
@ -121,6 +121,7 @@
|
||||
"sass.js": "0.11.1",
|
||||
"store": "2.0.12",
|
||||
"stream-browserify": "3.0.0",
|
||||
"tone": "^14.7.77",
|
||||
"typescript": "4.4.4",
|
||||
"uuid": "8.3.2",
|
||||
"validator": "13.6.0"
|
||||
|
@ -35,6 +35,7 @@ interface IShowSettingsProps {
|
||||
showLoading: boolean;
|
||||
submitNewAbout: () => void;
|
||||
toggleNightMode: (theme: string) => void;
|
||||
toggleSoundMode: (sound: boolean) => void;
|
||||
updateInternetSettings: () => void;
|
||||
updateIsHonest: () => void;
|
||||
updatePortfolio: () => void;
|
||||
@ -60,6 +61,7 @@ const mapDispatchToProps = {
|
||||
navigate,
|
||||
submitNewAbout,
|
||||
toggleNightMode: (theme: string) => updateUserFlag({ theme }),
|
||||
toggleSoundMode: (sound: boolean) => updateUserFlag({ sound }),
|
||||
updateInternetSettings: updateUserFlag,
|
||||
updateIsHonest: updateUserFlag,
|
||||
updatePortfolio: updateUserFlag,
|
||||
@ -75,6 +77,7 @@ export function ShowSettings(props: IShowSettingsProps): JSX.Element {
|
||||
isSignedIn,
|
||||
submitNewAbout,
|
||||
toggleNightMode,
|
||||
toggleSoundMode,
|
||||
user: {
|
||||
completedChallenges,
|
||||
email,
|
||||
@ -101,6 +104,7 @@ export function ShowSettings(props: IShowSettingsProps): JSX.Element {
|
||||
picture,
|
||||
points,
|
||||
theme,
|
||||
sound,
|
||||
location,
|
||||
name,
|
||||
githubProfile,
|
||||
@ -143,8 +147,10 @@ export function ShowSettings(props: IShowSettingsProps): JSX.Element {
|
||||
name={name}
|
||||
picture={picture}
|
||||
points={points}
|
||||
sound={sound}
|
||||
submitNewAbout={submitNewAbout}
|
||||
toggleNightMode={toggleNightMode}
|
||||
toggleSoundMode={toggleSoundMode}
|
||||
username={username}
|
||||
/>
|
||||
<Spacer />
|
||||
|
@ -6,9 +6,11 @@ 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';
|
||||
|
||||
import {
|
||||
closeDonationModal,
|
||||
isDonationModalOpenSelector,
|
||||
@ -76,6 +78,16 @@ 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;
|
||||
});
|
||||
}
|
||||
executeGA({ type: 'modal', data: '/donation-modal' });
|
||||
executeGA({
|
||||
type: 'event',
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import store from 'store';
|
||||
import { FlashState, State } from '../../../redux/types';
|
||||
|
||||
export const FlashApp = 'flash';
|
||||
@ -31,10 +32,32 @@ export type FlashMessageArg = {
|
||||
|
||||
export const createFlashMessage = (
|
||||
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,
|
||||
payload: { ...flash, id: nanoid() }
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const removeFlashMessage =
|
||||
(): ReducerPayload<FlashActionTypes.removeFlashMessage> => ({
|
||||
|
@ -10,6 +10,7 @@ import React, { Component } from 'react';
|
||||
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 UsernameSettings from './username';
|
||||
|
||||
@ -27,9 +28,11 @@ type AboutProps = {
|
||||
name: string;
|
||||
picture: string;
|
||||
points: number;
|
||||
sound: boolean;
|
||||
submitNewAbout: (formValues: FormValues) => void;
|
||||
t: TFunction;
|
||||
toggleNightMode: (theme: string) => void;
|
||||
toggleSoundMode: (sound: boolean) => void;
|
||||
username: string;
|
||||
};
|
||||
|
||||
@ -184,7 +187,14 @@ class AboutSettings extends Component<AboutProps, AboutState> {
|
||||
const {
|
||||
formValues: { name, location, picture, about }
|
||||
} = this.state;
|
||||
const { currentTheme, username, t, toggleNightMode } = this.props;
|
||||
const {
|
||||
currentTheme,
|
||||
sound,
|
||||
username,
|
||||
t,
|
||||
toggleNightMode,
|
||||
toggleSoundMode
|
||||
} = this.props;
|
||||
return (
|
||||
<div className='about-settings'>
|
||||
<UsernameSettings username={username} />
|
||||
@ -241,6 +251,7 @@ class AboutSettings extends Component<AboutProps, AboutState> {
|
||||
currentTheme={currentTheme}
|
||||
toggleNightMode={toggleNightMode}
|
||||
/>
|
||||
<SoundSettings sound={sound} toggleSoundMode={toggleSoundMode} />
|
||||
</FullWidthRow>
|
||||
</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 React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import store from 'store';
|
||||
|
||||
import ToggleSetting from './toggle-setting';
|
||||
|
||||
@ -26,9 +27,33 @@ export default function ThemeSettings({
|
||||
flagName='currentTheme'
|
||||
offLabel={t('buttons.off')}
|
||||
onLabel={t('buttons.on')}
|
||||
toggleFlag={() =>
|
||||
toggleNightMode(currentTheme === 'night' ? 'default' : 'night')
|
||||
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');
|
||||
}}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
|
@ -14,9 +14,9 @@ import { createGaSaga } from './ga-saga';
|
||||
|
||||
import hardGoToEpic from './hard-go-to-epic';
|
||||
import { createReportUserSaga } from './report-user-saga';
|
||||
|
||||
import { actionTypes as settingsTypes } from './settings/action-types';
|
||||
import { createShowCertSaga } from './show-cert-saga';
|
||||
import { createSoundModeSaga } from './sound-mode-saga';
|
||||
import updateCompleteEpic from './update-complete-epic';
|
||||
|
||||
export const MainApp = 'app';
|
||||
@ -74,7 +74,8 @@ export const sagas = [
|
||||
...createGaSaga(actionTypes),
|
||||
...createFetchUserSaga(actionTypes),
|
||||
...createShowCertSaga(actionTypes),
|
||||
...createReportUserSaga(actionTypes)
|
||||
...createReportUserSaga(actionTypes),
|
||||
...createSoundModeSaga({ ...actionTypes, ...settingsTypes })
|
||||
];
|
||||
|
||||
export const appMount = createAction(actionTypes.appMount);
|
||||
|
@ -118,6 +118,7 @@ export const User = PropTypes.shape({
|
||||
})
|
||||
),
|
||||
sendQuincyEmail: PropTypes.bool,
|
||||
sound: PropTypes.bool,
|
||||
theme: PropTypes.string,
|
||||
twitter: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
@ -295,6 +296,7 @@ export type UserType = {
|
||||
};
|
||||
progressTimestamps: Array<unknown>;
|
||||
sendQuincyEmail: boolean;
|
||||
sound: boolean;
|
||||
theme: string;
|
||||
twitter: 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 { createSelector } from 'reselect';
|
||||
import store from 'store';
|
||||
|
||||
import { Loader } from '../../../components/helpers';
|
||||
import { userSelector, isDonationModalOpenSelector } from '../../../redux';
|
||||
import {
|
||||
@ -27,7 +28,8 @@ import {
|
||||
ResizePropsType,
|
||||
Test
|
||||
} from '../../../redux/prop-types';
|
||||
|
||||
import { editorToneOptions } from '../../../utils/tone/editor-config';
|
||||
import { editorNotes } from '../../../utils/tone/editor-notes';
|
||||
import {
|
||||
canFocusEditorSelector,
|
||||
consoleOutputSelector,
|
||||
@ -44,6 +46,7 @@ import {
|
||||
|
||||
import './editor.css';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
|
||||
const MonacoEditor = Loadable(() => import('react-monaco-editor'));
|
||||
|
||||
interface EditorProps {
|
||||
@ -191,6 +194,17 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
const monacoRef: MutableRefObject<typeof monacoEditor | null> =
|
||||
useRef<typeof monacoEditor>(null);
|
||||
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
|
||||
// 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;
|
||||
|
||||
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?
|
||||
return { model };
|
||||
};
|
||||
@ -535,6 +557,21 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
coveringRange.startLineNumber - 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 });
|
||||
};
|
||||
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
take,
|
||||
cancel
|
||||
} from 'redux-saga/effects';
|
||||
import store from 'store';
|
||||
|
||||
import {
|
||||
buildChallenge,
|
||||
@ -97,10 +98,21 @@ 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 && payload?.showCompletionModal) {
|
||||
yield put(openModal('completion'));
|
||||
}
|
||||
|
||||
yield put(updateConsole(i18next.t('learn.tests-completed')));
|
||||
yield put(logsToConsole(i18next.t('learn.console-output')));
|
||||
} catch (e) {
|
||||
|
@ -5,6 +5,7 @@ import { connect } from 'react-redux';
|
||||
import ScrollableAnchor from 'react-scrollable-anchor';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import store from 'store';
|
||||
|
||||
import envData from '../../../../../config/env.json';
|
||||
import { isAuditedCert } from '../../../../../utils/is-audited';
|
||||
@ -56,6 +57,16 @@ export class Block extends Component {
|
||||
|
||||
handleBlockClick() {
|
||||
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({
|
||||
type: 'event',
|
||||
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