\n {props.children}\n \n )\n}\n\nexport {GameModeContextProvider, GameModeContext}\n","import React, {useState} from \"react\"\n\nconst MorseBufferContext = React.createContext()\n\nfunction MorseBufferContextProvider(props) {\n \n const [morseCharBuffer, setMorseCharBuffer] = useState('')\n const [morseWords, setMorseWords] = useState([])\n \n return (\n \n {props.children}\n \n )\n}\n\nexport {MorseBufferContextProvider, MorseBufferContext}\n","import React, {useState} from \"react\"\nimport alphabet from '../data/alphabet.json'\nimport numbers from '../data/numbers.json'\nimport common100 from '../data/common100.json'\nimport boys from '../data/names_boys.json'\nimport girls from '../data/names_girls.json'\nimport trek from '../data/startrek.json'\n\n\nconst WordListPickerContext = React.createContext()\n\nfunction WordListPickerContextProvider(props) {\n\n const [wordListCategory, setWordListCategory] = useState('alphabet')\n const [wordListCount, setWordListCount] = useState(10)\n\n let wordList = []\n\n switch (wordListCategory) {\n case \"alphabet\":\n wordList = alphabet.words\n break\n case \"numbers\":\n wordList = numbers.words\n break\n case \"boys\":\n wordList = boys.words\n break\n case \"girls\":\n wordList = girls.words\n break\n case \"startrek\":\n wordList = trek.words\n break\n case \"common100\":\n wordList = common100.words\n break\n default:\n wordList = alphabet.words\n }\n\n const wordListCountMax = wordList.length\n\n const metadata = {\n 'alphabet': {name: 'Alphabet', description: 'All letters of the alphabet'},\n 'numbers': {name: 'Numbers', description: '0-9'},\n 'boys': {name: 'Boys Names', description: 'Popular Boys Names'},\n 'girls': {name: 'Girls Names', description: 'Popular Girls Names'},\n 'startrek': {name: 'Star Trek', description: 'Star Trek universe'},\n 'common100': {name: 'Common Words', description: '100 Most Common Words'}\n }\n\n // Shuffle input array and return\n function randomize(arr) {\n let array = [...arr]\n let currentIndex = array.length, temporaryValue, randomIndex;\n \n // While there remain elements to shuffle...\n while (0 !== currentIndex) {\n \n // Pick a remaining element...\n randomIndex = Math.floor(Math.random() * currentIndex);\n currentIndex -= 1;\n \n // And swap it with the current element.\n temporaryValue = array[currentIndex];\n array[currentIndex] = array[randomIndex];\n array[randomIndex] = temporaryValue;\n }\n return array;\n }\n\n return (\n \n {props.children}\n \n )\n}\n\nexport {WordListPickerContextProvider, WordListPickerContext}\n","import React, {useState, useContext} from \"react\"\nimport { WordListPickerContext } from \"./wordListPickerContext\"\nconst WordFeederContext = React.createContext()\n\nfunction WordFeederContextProvider(props) {\n\n const {wordList, wordListShuffled} = useContext(WordListPickerContext)\n\n const [wordIndex, setWordIndex] = useState(0)\n const [order, setOrder] = useState('sequential')\n let word \n\n // Set word list ordered appropriately\n if (order === 'sequential') {\n if (wordList[wordIndex] === undefined) {\n word = [wordList[0]]\n }\n else {\n word = wordList[wordIndex]\n }\n }\n else if (order === 'random') {\n if (wordListShuffled[wordIndex] === undefined) {\n word = [wordListShuffled[0]]\n }\n else {\n word = wordListShuffled[wordIndex]\n }\n }\n\n function resetFeeder() {\n setWordIndex(0)\n }\n \n function getNextWord() {\n setWordIndex(prev => prev + 1)\n }\n \n return (\n \n {props.children}\n \n )\n}\n\nexport {WordFeederContextProvider, WordFeederContext}\n","import React, {useState, useContext} from \"react\"\nimport { WordFeederContext } from \"./wordFeederContext\"\nimport { MorseBufferContext } from \"./morseBufferContext\";\nimport morseCode from '../data/morse-reverse.json'\n\n\nconst ChallengeContext = React.createContext()\n\nfunction ChallengeContextProvider(props) {\n\n const [challengeState, setChallengeState] = useState('ready')\n const {resetFeeder} = useContext(WordFeederContext)\n const {word, getNextWord} = useContext(WordFeederContext)\n const {morseCharBuffer, setMorseCharBuffer} = useContext(MorseBufferContext)\n \n \n let morseArray = morseCharBuffer.split('_').filter(l => l !== '')\n let challengeWordClass = ''\n \n let correctCharIndexes = [] // Indexes of correct letters in Challenge Word\n let incorrectMorseIndexes = [] // Indexes of incorrect morse characters in morse character buffer\n \n let offset = 0\n let challengeLetters\n\n\n function startChallenge() {\n\n let countdown\n let count = 3\n\n // Challenge countdown setup\n document.getElementById('challengeReady').classList.add('starting')\n document.getElementById('challengeReady').innerHTML = `Challenge starting in ${count} `\n // Start Challenge countdown\n countdown = setInterval(() => {\n count--\n if (count === 0) {\n // Do this when countdown hits 0\n document.getElementById('challenge-overlay').classList.add('fade')\n clearInterval(countdown)\n setTimeout(() => {\n document.getElementById('challenge-overlay').classList.add('hide')\n // Start Challenge\n setChallengeState('started')\n }, 900);\n }\n document.getElementById('challengeReady').innerHTML = `Challenge starting in ${count} `\n }, 1000)\n }\n\n function completeChallenge() {\n if (challengeState !== 'completed') {\n setChallengeState('completed')\n resetFeeder()\n showOverlay()\n }\n }\n\n function cancelChallenge() {\n if (challengeState !== 'cancelled') {\n setChallengeState('cancelled')\n resetFeeder()\n showOverlay()\n }\n }\n\n function showOverlay() {\n const challengeOverlay = document.getElementById('challenge-overlay')\n challengeOverlay.classList.remove('fade')\n challengeOverlay.classList.remove('hide')\n }\n\n\n // If no more words in wordlist, feeder returns first word in an array\n if (typeof word === 'object') {\n completeChallenge()\n challengeLetters = word[0].split('')\n }\n else {\n challengeLetters = word.split('')\n }\n\n // Iterate through the morse character buffer and compare with each letter of challenge word\n morseArray.forEach((item, index) => { \n if (morseCharBuffer.slice(-1) === '_') { // If end of morse character\n \n let morseLetter = morseCode[morseArray[index]] || '[?]'\n let challengeLetter = challengeLetters[index-offset].toLowerCase()\n \n if (morseLetter === challengeLetter) {\n correctCharIndexes.push(index-offset)\n \n document.getElementById('challengeWord').childNodes[index-offset].classList.add('correct')\n }\n else {\n incorrectMorseIndexes.push(index)\n if (incorrectMorseIndexes.length > 0) {\n setMorseCharBuffer(prev => {\n let newState = prev.split('_').filter(l => l !== '')\n newState.splice(incorrectMorseIndexes[0], 1)\n newState = newState.join('_') + '_'\n \n return newState\n })\n incorrectMorseIndexes.splice(1,incorrectMorseIndexes.length)\n }\n offset = incorrectMorseIndexes.length\n }\n }\n })\n\n\n // Retrieve next word once all characters are correct\n if (correctCharIndexes.length === challengeLetters.length) {\n challengeWordClass = 'correct'\n setTimeout(() => {\n setMorseCharBuffer('')\n morseArray = []\n incorrectMorseIndexes = []\n offset = 0\n if (document.getElementById('challengeWord') !== null) {\n document.getElementById('challengeWord').childNodes.forEach(node => {\n node.classList = \"cLetter\"\n })\n }\n }, 800)\n setTimeout(() => {\n if (correctCharIndexes.length > 0) {\n correctCharIndexes = []\n getNextWord()\n }\n }, 1000)\n }\n\n \n return (\n \n {props.children}\n \n )\n}\n\nexport {ChallengeContextProvider, ChallengeContext}\n","import React, {useState, useContext, useEffect} from \"react\"\nimport { ChallengeContext } from \"./challengeContext\"\nconst GameClockContext = React.createContext()\n\nfunction GameClockContextProvider(props) {\n\n const [gameClockTime, setGameClockTime] = useState(0)\n const [clockIsRunning, setClockIsRunning] = useState(false)\n const [intervals, setIntervals] = useState([])\n const {challengeState, setChallengeState} = useContext(ChallengeContext)\n\n\n function startGameClock() {\n if (!clockIsRunning) {\n setClockIsRunning(true)\n setIntervals(prev => [...prev, (setInterval(() => {\n if (document.getElementById('gameClock') === null) {\n stopGameClock()\n return\n }\n setGameClockTime(prev => prev + 1)\n }, 1000))\n ])\n }\n }\n function stopGameClock() {\n if (clockIsRunning) {\n cleanup()\n setClockIsRunning(false)\n }\n }\n\n // Clear game clock intervals\n function cleanup() {\n for (let i = 0; i < intervals.length; i++) {\n clearInterval(intervals[i]);\n }\n }\n\n // Trigger game clock changes on challenge state change\n useEffect(() => {\n switch (challengeState) {\n case 'ready':\n setGameClockTime(0)\n cleanup()\n break\n case 'started':\n startGameClock()\n break\n case 'completed':\n stopGameClock()\n break\n case 'cancelled':\n stopGameClock()\n setChallengeState('ready')\n break\n default:\n return\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [challengeState])\n \n\n return (\n \n {props.children}\n \n )\n}\n\nexport {GameClockContextProvider, GameClockContext}\n","import React, {useState} from \"react\"\n\nconst WPMContext = React.createContext()\n\nfunction WPMContextProvider(props) {\n \n const [wpm, setWPM] = useState(10)\n\n return (\n \n {props.children}\n \n )\n}\n\nexport {WPMContextProvider, WPMContext}\n","import React, {useState} from \"react\"\n\nconst FrequencyContext = React.createContext()\n\nfunction FrequencyContextProvider(props) {\n \n const [frequency, setFrequency] = useState(650)\n\n return (\n \n {props.children}\n \n )\n}\n\nexport {FrequencyContextProvider, FrequencyContext}\n","import React, {useState} from \"react\"\n\nconst KeyTypeContext = React.createContext()\n\nfunction KeyTypeContextProvider(props) {\n\n const [keyType, setKeyType] = useState('straight')\n\n return (\n \n {props.children}\n \n )\n}\n\nexport {KeyTypeContextProvider, KeyTypeContext}\n","import {useEffect, useContext} from 'react'\nimport { FrequencyContext } from '../contexts/frequencyContext'\nimport { GameModeContext } from '../contexts/gameModeContext'\nimport { MorseBufferContext } from '../contexts/morseBufferContext'\nimport { WPMContext } from '../contexts/wpmContext'\nimport config from '../config.json'\n\n// ELECTRONIC KEY TELEGRAPH - Iambic A\n\nexport default (function useElectronicKey() {\n\n const {morseCharBuffer, setMorseCharBuffer, morseWords, setMorseWords} = useContext(MorseBufferContext)\n const {wpm} = useContext(WPMContext)\n const {gameMode} = useContext(GameModeContext)\n const {frequency} = useContext(FrequencyContext)\n\n // DitDah length setup\n let ditMaxTime = 1200/wpm\n let ratio = .2\n const letterGapMinTime = ditMaxTime*ratio*3\n const wordGapMaxTime = ditMaxTime*ratio*7\n\n const morseHistorySize = config.historySize\n \n let leftIsPressed = false\n let rightIsPressed = false\n let queueRunning = false\n let queue = []\n let pressedFirst = null\n let lastPlayed = ''\n\n // Timers setup\n let depressSyncTime\n let depressSyncTimer\n let depressSyncTimerRunning = false\n let gapTime = 0\n let gapTimer = 0\n\n let paddlesReleasedSimultaneously = false\n\n let currentPromise = Promise.resolve()\n\n // Audio Setup\n let AudioContext = window.AudioContext || window.webkitAudioContext || false\n let context\n window.AudioContext = window.AudioContext || window.webkitAudioContext;\n if (AudioContext) {\n context = new AudioContext()\n } else {\n context = null\n }\n\n // Promisify playing Dits and Dahs\n function play(ditDah) {\n lastPlayed = ditDah\n let playDuration = ((ditDah === '.') ? ditMaxTime : ditMaxTime*3)\n\n return new Promise((resolve, reject) => {\n if (context.state === 'interrupted') {\n context.resume()\n }\n \n let o = context.createOscillator()\n o.frequency.value = frequency\n o.type = \"sine\"\n o.onended = () => {\n resolve()\n }\n \n let startTime = context.currentTime;\n \n let g = context.createGain()\n g.gain.exponentialRampToValueAtTime(config.mainVolume, startTime)\n g.gain.setValueAtTime(config.mainVolume, startTime)\n o.connect(g)\n g.connect(context.destination)\n \n o.start(startTime)\n \n setTimeout(() => {\n g.gain.setTargetAtTime(0.0001, context.currentTime, 0.001)\n o.stop(context.currentTime + 0.05)\n }, playDuration)\n })\n }\n\n // Play dit or dah with trailing space (silence)\n function playWithSpaces(ditDah) {\n let delay = (ditDah === '.') ? ditMaxTime + ditMaxTime : ditMaxTime*3 + ditMaxTime\n\n return new Promise(function(resolve) {\n if (ditDah === '.' || ditDah === '-') {\n\n clearInterval(gapTimer)\n checkGapBetweenInputs()\n setMorseCharBuffer(prev => prev + ditDah)\n \n play(ditDah)\n .then(setTimeout(() => {\n // START GAP TIMER\n gapTimer = setInterval(() => {\n gapTime += 1\n \n // if (gapTime >= wordGapMaxTime) {\n if (gameMode === 'practice' && gapTime >= wordGapMaxTime) {\n setMorseCharBuffer(prev => prev + '/')\n clearInterval(gapTimer)\n gapTimer = 0\n gapTime = 0\n }\n else if (gameMode === 'challenge' && gapTime >= letterGapMinTime) {\n setMorseCharBuffer(prev => prev + '_')\n clearInterval(gapTimer)\n gapTimer = 0\n gapTime = 0\n }\n }, 1)\n \n resolve();\n }, delay)\n )\n } else {\n setTimeout(() => {\n resolve();\n }, ditMaxTime*3)\n }\n });\n }\n\n function executeQueue() {\n let localQueue = queue\n \n // Set waitTime to completion of queue (ditDah time + following silences)\n let waitTime = 0\n for (let i in localQueue) {\n if (localQueue[i] === '.') {\n waitTime += ditMaxTime*2\n } else if (localQueue[i] === '-') {\n waitTime += ditMaxTime*4\n }\n }\n \n // Cleanup\n function cleanup() {\n queueRunning = false\n queue = []\n sendPressedToQueue() // Check if anything still pressed down on queue finish\n }\n \n // Wait till completion of queue to execute\n const clear = setTimeout(() => {\n cleanup()\n }, waitTime)\n \n // Execute queue\n queueRunning = true\n for (let i = 0; i < localQueue.length; i++) {\n if (paddlesReleasedSimultaneously) {\n localQueue.pop()\n clearTimeout(clear)\n cleanup()\n }\n currentPromise = currentPromise.then(() => {\n var ditDah = localQueue[i]\n if (ditDah)\n {\n return playWithSpaces(ditDah)\n }\n });\n }\n }\n\n // Determine which paddles are pressed, add to queue, and execute\n function sendPressedToQueue() {\n if (leftIsPressed && rightIsPressed) {\n if (pressedFirst === 'left') {\n queue.push('-')\n pressedFirst = null\n } else {\n queue.push('.')\n queue.push('-')\n }\n }\n else if (leftIsPressed && !rightIsPressed) {\n queue.push('.')\n }\n else if (rightIsPressed && !leftIsPressed) {\n queue.push('-')\n }\n if (queue.length > 0) {\n executeQueue()\n }\n }\n\n\n function handleInputStart(event) {\n if (event.type === 'touchstart') {\n event.preventDefault()\n }\n\n paddlesReleasedSimultaneously = false\n\n if (event.keyCode === 188 || event.keyCode === 190) {\n if (document.activeElement.id === 'morseInput') {\n return\n } else if (document.activeElement.tagName.toLowerCase() !== 'body') {\n event.preventDefault()\n document.activeElement.blur()\n }\n }\n if (event.repeat) { return }\n\n if (event.keyCode === 188 || event.target.id === \"left\") {\n document.querySelector('.paddle#left').classList.add('active')\n \n leftIsPressed = true\n if (!rightIsPressed) { pressedFirst = 'left'}\n\n // Prevent further input if queue is executing\n if (!queueRunning) {\n sendPressedToQueue()\n }\n }\n else if (event.keyCode === 190 || event.target.id === \"right\") {\n document.querySelector('.paddle#right').classList.add('active')\n\n rightIsPressed = true\n if (!leftIsPressed) { pressedFirst = 'right'}\n\n // Prevent further input if queue is executing\n if (!queueRunning) {\n sendPressedToQueue()\n }\n }\n }\n\n function handleInputEnd(event) {\n if (event.type === 'touchend') {\n event.preventDefault()\n }\n\n if (event.keyCode === 188 || event.target.id === \"left\") {\n document.querySelector('.paddle#left').classList.remove('active')\n\n leftIsPressed = false\n if (pressedFirst === 'left') { pressedFirst = null }\n\n if (!depressSyncTimerRunning) { startDepressSyncTimer() }\n else { stopDepressSyncTimer() }\n }\n if (event.keyCode === 190 || event.target.id === \"right\") {\n document.querySelector('.paddle#right').classList.remove('active')\n\n rightIsPressed = false\n if (pressedFirst === 'right') { pressedFirst = null }\n\n if (!depressSyncTimerRunning) { startDepressSyncTimer() }\n else { stopDepressSyncTimer() }\n }\n\n if (paddlesReleasedSimultaneously && document.getElementById('modeB').classList.contains('selected'))\n {\n currentPromise = currentPromise.then(() => {\n return playWithSpaces(lastPlayed == '.' ? '-' : '.')\n });\n }\n }\n \n // Timer used to determine if both paddles are released within 10ms\n // Need to know this to stop Iambic tones at correct time\n function startDepressSyncTimer() {\n depressSyncTimerRunning = true\n // Reset depressSyncTime\n depressSyncTime = 0\n // Start depressSyncTimer\n depressSyncTimer = setInterval(() => {\n depressSyncTime += 1\n if (depressSyncTime > 20) {\n depressSyncTimerRunning = false\n clearInterval(depressSyncTimer)\n depressSyncTime = 0\n }\n }, 1);\n }\n function stopDepressSyncTimer() {\n depressSyncTimerRunning = false\n clearInterval(depressSyncTimer)\n if (depressSyncTime < 10) {\n paddlesReleasedSimultaneously = true\n queue.pop()\n }\n depressSyncTime = 0\n }\n\n\n // Check gap between letters to determin if new character or new word\n function checkGapBetweenInputs() {\n if (gapTime >= letterGapMinTime && gapTime < wordGapMaxTime) {\n if (gameMode === 'practice') {\n setMorseCharBuffer(prev => prev + ' ')\n } else if (gameMode === 'challenge') {\n setMorseCharBuffer(prev => prev + '_')\n }\n gapTime = 0\n clearInterval(gapTimer)\n gapTimer = 0\n }\n }\n\n // Add paddle event listeners and update on WPM, Game Mode, or Frequency change\n // Not updating on these state changes prevents change from taking effect\n useEffect(() => {\n document.addEventListener('keydown', handleInputStart)\n document.addEventListener('keyup', handleInputEnd)\n\n const paddles = document.querySelectorAll('.paddle')\n paddles.forEach(paddle => {\n paddle.addEventListener('mousedown', handleInputStart)\n paddle.addEventListener('touchstart', handleInputStart)\n paddle.addEventListener('mouseout', handleInputEnd)\n paddle.addEventListener('mouseup', handleInputEnd)\n paddle.addEventListener('touchend', handleInputEnd)\n })\n\n return function cleanup() {\n document.removeEventListener('keydown', handleInputStart)\n document.removeEventListener('keyup', handleInputEnd)\n\n const paddles = document.querySelectorAll('.paddle')\n paddles.forEach(paddle => {\n paddle.removeEventListener('mousedown', handleInputStart)\n paddle.removeEventListener('touchstart', handleInputStart)\n paddle.removeEventListener('mouseout', handleInputEnd)\n paddle.removeEventListener('mouseup', handleInputEnd)\n paddle.removeEventListener('touchend', handleInputEnd)\n })\n\n clearInterval(depressSyncTimer)\n clearInterval(gapTimer)\n }\n // eslint-disable-next-line\n }, [wpm, gameMode, frequency])\n\n // Remove forward slash and move buffer contents to morse words array\n useEffect(() => {\n if (morseCharBuffer.slice(-1) === '/' && gameMode === 'practice') {\n let val = morseCharBuffer.slice(0,morseCharBuffer.length-1)\n setMorseWords(prev => [val, ...prev])\n\n // Limit history to configured history size\n if (morseWords.length >= morseHistorySize) {\n setMorseWords(prev => prev.slice(0,prev.length-1))\n }\n setMorseCharBuffer('')\n }\n\n // eslint-disable-next-line\n }, [morseCharBuffer])\n\n})","import React from \"react\"\nimport useElectronicKey from '../hooks/useElectronicKey';\n\nexport default React.memo(function ElectronicKey() {\n useElectronicKey()\n})","import React from \"react\"\n\nexport default (function DitDahDisplay(props) {\n\n return (props.dd === ' ') ?
: {props.dd}
\n})","import React, { useContext } from \"react\"\nimport { MorseBufferContext } from \"../contexts/morseBufferContext\"\nimport DitDahDisplay from \"./DitDahDisplay\"\nimport morseCode from '../data/morse-reverse.json'\n\nexport default React.memo(function MorseBufferDisplay() {\n \n const {morseCharBuffer} = useContext(MorseBufferContext)\n\n let ditDahs = morseCharBuffer.split('').map((ditdah,index) => )\n let alphanumeric = ''\n let letters = morseCharBuffer.split(' ')\n\n if (morseCharBuffer === '') {}\n else {\n for (let i in letters) {\n if (letters[i] === ' ') {\n alphanumeric += ' '\n } else {\n if (morseCode[letters[i]] === undefined) {\n alphanumeric += (letters[i] === '' ? '':'[?]')\n } else {\n alphanumeric += morseCode[letters[i]]\n }\n }\n }\n }\n\n return (\n \n
\n
\n
\n
\n {alphanumeric.toUpperCase()}\n
\n
\n
\n )\n})","import React, {useContext} from \"react\"\nimport { MorseBufferContext } from \"../contexts/morseBufferContext\"\nimport morseCode from '../data/morse-reverse.json'\n\nexport default (function MorseHistoryTextBox() {\n\n const {morseWords, setMorseWords} = useContext(MorseBufferContext)\n\n let text = ''\n\n function clearHistory() {\n setMorseWords([])\n }\n\n // Generate Morse History contents\n morseWords.forEach((word, index) => {\n if (word.includes(' ')) {\n let newWord = ''\n word.split(' ').forEach(letter => {\n if (morseCode[letter] === undefined) {\n newWord += '[?]'\n } else {\n newWord += morseCode[letter].toUpperCase()\n }\n })\n text = newWord + ' ' + text\n }\n else if (morseCode[word] === undefined) {\n text = '[?] ' + text\n } else {\n text = morseCode[word].toUpperCase() + ' ' + text\n }\n })\n\n return (\n \n
{text}
\n
\n \"[?] \" signifies no translation available. \n Clear \n
\n
\n )\n})","import { useEffect, useContext } from 'react'\nimport { FrequencyContext } from '../contexts/frequencyContext'\nimport { GameModeContext } from '../contexts/gameModeContext'\nimport { MorseBufferContext } from '../contexts/morseBufferContext'\nimport { WPMContext } from '../contexts/wpmContext'\nimport config from '../config.json'\n\n// STRAIGHT KEY TELEGRAPH\nexport default (function useStraightKey() {\n \n const {morseCharBuffer, setMorseCharBuffer, morseWords, setMorseWords} = useContext(MorseBufferContext)\n const {wpm} = useContext(WPMContext)\n const {gameMode} = useContext(GameModeContext)\n const {frequency} = useContext(FrequencyContext)\n\n // Spacing time and timer setup\n let charTimer = 0\n let charTime = 0\n let gapTimer = 0\n let gapTime = 0\n \n // DitDah Length\n const ditMaxTime = 1200/wpm * 0.3\n const letterGapMinTime = ditMaxTime*3\n const wordGapMaxTime = ditMaxTime*7\n\n const morseHistorySize = config.historySize\n\n // Tone Setup\n let AudioContext = window.AudioContext || window.webkitAudioContext || false\n let context\n window.AudioContext = window.AudioContext || window.webkitAudioContext;\n if (AudioContext) {\n context = new AudioContext()\n } else {\n context = null\n }\n\n let o // Oscillator Node\n let g // Gain Node\n\n let isRunning = false\n\n function handleInputStart(event) {\n if (event.type === 'touchstart') {\n event.preventDefault()\n }\n\n if (event.keyCode === 32) {\n if (document.activeElement.id === 'morseInput') {\n return\n }\n else if (document.activeElement.tagName.toLowerCase() !== 'body') {\n event.preventDefault()\n document.activeElement.blur()\n }\n }\n\n if (isRunning) {\n return\n } else {\n isRunning = true\n\n if ((event.keyCode !== 32 &&\n event.target.id !== \"morseButton\" &&\n event.target.className !== \"paddle\") ||\n (event.repeat)) {\n return\n }\n else {\n document.getElementById('morseButton').classList.add('active')\n \n // isRunning = true\n \n if (context.state === 'interrupted') {\n context.resume()\n }\n \n o = context.createOscillator()\n o.frequency.value = frequency\n o.type = \"sine\"\n \n g = context.createGain()\n g.gain.exponentialRampToValueAtTime(config.mainVolume, context.currentTime)\n o.connect(g)\n g.connect(context.destination)\n o.start()\n \n checkGapBetweenInputs()\n clearInterval(gapTimer)\n \n startCharTimer()\n }\n }\n \n }\n \n function startCharTimer() {\n // Start Character Timer\n charTimer = setInterval(() => {\n charTime += 1\n }, 1);\n }\n\n function handleInputEnd(event) {\n if (isRunning) {\n if ((event.keyCode !== 32 &&\n event.target.id !== \"morseButton\" &&\n event.target.className !== \"paddle\") ||\n (event.repeat)) {\n return\n }\n\n document.getElementById('morseButton').classList.remove('active')\n\n isRunning = false\n \n if (charTime <= ditMaxTime) {\n setMorseCharBuffer(prev => prev + '.')\n } else {\n setMorseCharBuffer(prev => prev + '-')\n }\n \n stopCharTimer()\n startGapTimer()\n \n // Account for bug triggered when pressing paddle button (e.g.) outside of body, then clicking into body, and depressing key\n if (o === undefined) { \n return\n }\n if (o.context.state === 'running') {\n g.gain.setTargetAtTime(0.0001, context.currentTime, 0.001)\n o.stop(context.currentTime + 0.05)\n }\n } else { return }\n }\n\n function stopCharTimer() { \n clearInterval(charTimer)\n charTimer = 0\n charTime = 0\n }\n\n function startGapTimer() {\n gapTime = 0\n gapTimer = setInterval(() => {\n gapTime += 1\n\n // Gap between words\n if (gameMode === 'practice' && gapTime >= wordGapMaxTime) {\n setMorseCharBuffer(prev => prev + '/')\n clearInterval(gapTimer)\n gapTimer = 0\n gapTime = 0\n }\n else if (gameMode === 'challenge' && gapTime >= letterGapMinTime) {\n setMorseCharBuffer(prev => prev + '_')\n clearInterval(gapTimer)\n gapTimer = 0\n gapTime = 0\n }\n }, 1);\n }\n\n // Check gap between letters to determin if new character or new word\n function checkGapBetweenInputs() {\n if (gapTime >= letterGapMinTime && gapTime < wordGapMaxTime) {\n if (gameMode === 'practice') {\n setMorseCharBuffer(prev => prev + ' ')\n } else if (gameMode === 'challenge') {\n setMorseCharBuffer(prev => prev + '_')\n }\n clearInterval(gapTimer)\n gapTimer = 0\n }\n }\n \n // Add paddle event listeners and update on WPM, Game Mode, or Frequency change\n // Not updating on these state changes prevents change from taking effect\n useEffect(() => {\n document.addEventListener('keydown', handleInputStart)\n document.addEventListener('keyup', handleInputEnd)\n\n const morseButton = document.getElementById('morseButton')\n morseButton.addEventListener('touchstart', handleInputStart)\n morseButton.addEventListener('touchend', handleInputEnd)\n morseButton.addEventListener('mousedown', handleInputStart)\n morseButton.addEventListener('mouseout', handleInputEnd)\n morseButton.addEventListener('mouseup', handleInputEnd)\n\n return function cleanup() {\n document.removeEventListener('keydown', handleInputStart)\n document.removeEventListener('keyup', handleInputEnd)\n\n const morseButton = document.getElementById('morseButton')\n morseButton.removeEventListener('touchstart', handleInputStart)\n morseButton.removeEventListener('touchend', handleInputEnd)\n morseButton.removeEventListener('mousedown', handleInputStart)\n morseButton.removeEventListener('mouseout', handleInputEnd)\n morseButton.removeEventListener('mouseup', handleInputEnd)\n\n clearInterval(charTimer)\n clearInterval(gapTimer)\n }\n // eslint-disable-next-line\n }, [wpm, gameMode, frequency])\n\n // Remove forward slash and move buffer contents to morse words array\n useEffect(() => {\n if (morseCharBuffer.slice(-1) === '/' && gameMode === 'practice') {\n let val = morseCharBuffer.slice(0,morseCharBuffer.length-1)\n setMorseWords(prev => [val, ...prev])\n\n // Limit history to configured history size\n if (morseWords.length >= morseHistorySize) {\n setMorseWords(prev => prev.slice(0,prev.length-1))\n }\n setMorseCharBuffer('')\n\n // Scroll morse history textbox to bottom (scrolling enabled on mobile screens)\n let morseHistory = document.getElementById(\"morseHistory-textbox\");\n morseHistory.scrollTop = morseHistory.scrollHeight;\n }\n\n // eslint-disable-next-line\n }, [morseCharBuffer])\n\n})","import React from \"react\"\nimport useStraightKey from '../hooks/useStraightKey';\n\nexport default React.memo(function StraightKey() {\n useStraightKey()\n})","import React, { useContext } from 'react';\nimport '../css/App.css';\nimport { KeyTypeContext } from '../contexts/keyTypeContext';\nimport ElectronicKey from '../components/ElectronicKey';\nimport MorseBufferDisplay from '../components/MorseBufferDisplay'\nimport MorseHistoryTextBox from '../components/MorseHistory'\nimport StraightKey from '../components/StraightKey';\n\n\nexport default (function PracticeMode() {\n\n const {keyType} = useContext(KeyTypeContext)\n\n return (\n <>\n {keyType === \"straight\" ?\n : \n }\n \n \n >\n );\n\n \n})","import React from \"react\"\n\nexport default React.memo(function ChallengeBufferDisplay(props) {\n\n const morseArray = props.morseArray\n\n let ditDahs = []\n\n for (let i in morseArray) {\n let morseChar = morseArray[i]\n \n ditDahs.push({morseChar} )\n }\n\n return (\n \n )\n})","import React from \"react\"\n\nexport default (function ChallengeControls(props) {\n\n return (\n \n Exit Challenge \n
\n )\n})","import React, { useContext } from \"react\"\nimport { WordFeederContext } from \"../contexts/wordFeederContext\"\n\nexport default React.memo(function ChallengeWord(props) {\n\n const {word} = useContext(WordFeederContext)\n\n let challengeLetters\n if (typeof word === 'object') {\n challengeLetters = word[0].split('')\n }\n else {\n challengeLetters = word.split('')\n }\n\n let spannedWord = challengeLetters.map((letter,index) => {letter} )\n\n return (\n {spannedWord}
\n )\n})","import React, {useContext} from \"react\"\nimport { GameClockContext } from \"../contexts/gameClockContext\";\n\nexport default (function GameClock(props) {\n \n const {gameClockTime} = useContext(GameClockContext)\n\n const minutes = Math.floor(gameClockTime / 60)\n const seconds = gameClockTime % 60\n\n return (\n Time Elapsed: {minutes} minutes {seconds} seconds
\n )\n})","import React, {useContext} from 'react';\nimport '../css/App.css';\nimport { ChallengeContext } from '../contexts/challengeContext';\nimport { KeyTypeContext } from '../contexts/keyTypeContext';\nimport ChallengeBufferDisplay from '../components/ChallengeBufferDisplay';\nimport ChallengeControls from '../components/ChallengeControls';\nimport ChallengeWord from '../components/ChallengeWord'\nimport ElectronicKey from '../components/ElectronicKey';\nimport GameClock from '../components/GameClock';\nimport StraightKey from '../components/StraightKey';\n\n\nexport default React.memo(function ChallengeMode() {\n \n const {keyType} = useContext(KeyTypeContext)\n const {challengeState, cancelChallenge, morseArray, challengeWordClass} = useContext(ChallengeContext)\n\n return (\n <>\n {challengeState === 'started' ? (keyType === \"straight\" ?\n : ) : <>>\n }\n \n \n \n >\n )\n});\n","import React, {useContext} from \"react\"\nimport { ChallengeContext } from \"../contexts/challengeContext\"\nimport { GameClockContext } from \"../contexts/gameClockContext\"\nimport { GameModeContext } from \"../contexts/gameModeContext\"\nimport { MorseBufferContext } from \"../contexts/morseBufferContext\"\nimport { WordFeederContext } from \"../contexts/wordFeederContext\"\n\nexport default React.memo(function ModePicker() {\n\n const {setGameMode} = useContext(GameModeContext)\n const {setMorseCharBuffer} = useContext(MorseBufferContext)\n const {resetFeeder} = useContext(WordFeederContext)\n const {stopGameClock, setGameClockTime, clockIsRunning} = useContext(GameClockContext)\n const {setChallengeState} = useContext(ChallengeContext)\n\n function handleClick(e) {\n setMorseCharBuffer('')\n resetFeeder()\n setChallengeState('ready')\n\n setGameMode(e.target.id)\n\n if (clockIsRunning) { \n stopGameClock()\n setGameClockTime(0)\n }\n\n let buttons = document.querySelector(\".mode-picker#gameMode #buttons\").childNodes\n buttons.forEach(button => {\n if (button.id === e.target.id) {\n button.classList.add('selected')\n } else { button.classList.remove('selected')}\n })\n }\n\n return (\n \n
\n Mode\n
\n
\n \n Practice Mode\n \n \n Challenge Mode\n \n
\n
\n )\n})","import React, {useContext, useEffect} from \"react\"\nimport {KeyTypeContext} from \"../contexts/keyTypeContext\"\n\n\nexport default React.memo(function KeyTypePicker() {\n\n const {setKeyType, keyType} = useContext(KeyTypeContext)\n\n function handleClick(e) {\n setKeyType(e.target.id)\n switch (e.target.id)\n {\n case 'straight':\n e.target.classList.add('selected');\n document.getElementById('electronic').classList.remove('selected')\n document.querySelector('#morseButton').classList.remove('showPaddles')\n document.querySelector('.paddle').classList.remove('showPaddles')\n document.querySelector('.paddle#left').classList.remove('showPaddles')\n document.querySelector('.paddle#right').classList.remove('showPaddles')\n document.getElementById('paddle-mode-buttons').style.visibility = 'hidden'\n document.getElementById('morseButtonText').innerHTML = 'TAP BUTTON OR PRESS SPACEBAR'\n break;\n case 'electronic':\n e.target.classList.add('selected');\n document.getElementById('straight').classList.remove('selected')\n document.querySelector('#morseButton').classList.add('showPaddles')\n document.querySelector('.paddle').classList.add('showPaddles')\n document.querySelector('.paddle#left').classList.add('showPaddles')\n document.querySelector('.paddle#right').classList.add('showPaddles')\n document.getElementById('paddle-mode-buttons').style.visibility = 'visible'\n document.getElementById('morseButtonText').innerHTML = 'TAP/HOLD BUTTONS OR PRESS COMMA / PERIOD'\n break\n case 'modeA':\n e.target.classList.add('selected')\n document.getElementById('modeB').classList.remove('selected')\n break;\n case 'modeB':\n e.target.classList.add('selected')\n document.getElementById('modeA').classList.remove('selected')\n break;\n }\n }\n\n useEffect(() => {\n document.querySelector(`button#${keyType}`).classList.add('selected')\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n return (\n \n
\n Key Type\n
\n
\n \n Straight Key\n \n \n Electronic Key\n \n
\n
\n
\n Paddle Mode \n \n A\n \n \n B\n \n \n \n \n
\n )\n})","const shareLinks = {\n 'twitter': {\n name: 'Twitter',\n icon: 'ri-twitter-fill',\n link:'https://twitter.com/intent/tweet?text=Check%20out%20this%20site%20that%20helps%20you%20learn%20Morse%20Code%3A%20https%3A//learnmorsecode.net%20%40genemecija%20%23morse%20%23morsecode'\n },\n 'facebook': {\n name: 'Facebook',\n icon: 'ri-facebook-box-fill',\n link: 'https://www.facebook.com/sharer/sharer.php?u=https%3A//learnmorsecode.net'\n },\n 'email': {\n name: 'Email',\n icon: \"ri-mail-line\",\n link: 'mailto:?subject='+encodeURIComponent('Check out this site that helps you learn Morse code! https://learnmorsecode.net')\n }\n}\nconst contactLinks = {\n 'email': {\n name: 'Email',\n icon: \"ri-mail-line\",\n link: 'mailto:gene@genemecija.com?subject='+encodeURIComponent('Hello, Gene!')\n },\n 'github': {\n name: 'GitHub',\n icon: 'ri-github-fill',\n link: 'https://github.com/genemecija/learn-morse-code/'\n },\n 'twitter': {\n name: 'Twitter',\n icon: 'ri-twitter-fill',\n link:'https://twitter.com/genemecija'\n }\n}\n\nexport {shareLinks, contactLinks}","import React from \"react\"\nimport { shareLinks } from \"../data/social\"\n\nexport default (function Header () {\n\n function PopupCenter(url, title, w, h) { \n // Credit: http://www.xtf.dk/2011/08/center-new-popup-window-even-on.html\n \n // Fixes dual-screen position Most browsers Firefox \n const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : window.screen.left; \n const dualScreenTop = window.screenTop !== undefined ? window.screenTop : window.screen.top; \n \n const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : window.screen.width; \n let height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : window.screen.height; \n \n const left = ((width / 2) - (w / 2)) + dualScreenLeft; \n const top = ((height / 2) - (h / 2)) + dualScreenTop; \n const newWindow = window.open(url, title, 'scrollbars=yes, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left); \n \n // Puts focus on the newWindow \n if (window.focus) { \n newWindow.focus(); \n } \n }\n \n function handleClick(event) {\n let link = event.target.id\n let url = shareLinks[link]['link']\n let title = 'Share'\n let width = '900'\n let height = '500'\n if (link === 'email') {\n width = '150'\n height = '150'\n }\n PopupCenter(url, title, width, height)\n }\n\n let contacts = Object.keys(shareLinks).map((contact, index) => {\n return (\n \n )\n })\n\n return (\n \n )\n})","import { useContext } from 'react';\nimport config from '../config.json'\nimport { WPMContext } from '../contexts/wpmContext.js';\nimport { FrequencyContext } from '../contexts/frequencyContext.js';\n\nexport default (function useMorsePlayer() {\n\n const {wpm} = useContext(WPMContext)\n const {frequency} = useContext(FrequencyContext)\n // const ditMaxTime = 85 //config.ditMaxTime\n const ditMaxTime = 1200/wpm\n\n // Tone Setup\n let AudioContext = window.AudioContext || window.webkitAudioContext\n window.AudioContext = window.AudioContext || window.webkitAudioContext;\n let context\n if (AudioContext) {\n context = new AudioContext()\n } else {\n context = null\n }\n\n // Play dit or dah\n function play(ditDah) {\n let length = ((ditDah === '.') ? ditMaxTime : ditMaxTime*3)\n \n if (context.state === 'interrupted') {\n context.resume()\n }\n let o\n o = context.createOscillator()\n o.frequency.value = frequency\n o.type = \"sine\"\n \n let startTime = context.currentTime;\n\n let g = context.createGain()\n g.gain.exponentialRampToValueAtTime(config.mainVolume, startTime)\n g.gain.setValueAtTime(config.mainVolume, startTime)\n o.connect(g)\n g.connect(context.destination)\n o.start(startTime)\n \n setTimeout(() => {\n g.gain.setTargetAtTime(0.0001, context.currentTime, 0.009)\n o.stop(context.currentTime + 0.05)\n }, length)\n }\n\n let queue = []\n let timeouts = []\n \n function playMorseWord(morse) {\n // Empty morse queue and cancel all sounds (timeouts)\n queue = []\n for (let i = 0; i < timeouts.length; i++) {\n clearTimeout(timeouts[i]);\n }\n\n queue = Array.from(morse)\n let delay = 0\n let firstWord = true\n\n // Iterate through queue, playing dits/dahs sequentially after appropriate delays\n for (let i = 0; i < queue.length; i++) {\n let char = queue[i]\n if (char === '.') {\n if (firstWord) {\n firstWord = false\n timeouts.push(setTimeout(() => {\n play(char)\n }, 0))\n } else {\n timeouts.push(setTimeout(() => {\n play(char)\n }, delay))\n }\n delay += ditMaxTime*2\n } else if (char === '-') {\n if (firstWord) {\n firstWord = false\n timeouts.push(setTimeout(() => {\n play(char)\n }, 0))\n } else {\n timeouts.push(setTimeout(() => {\n play(char)\n }, delay))\n }\n delay += ditMaxTime*4\n } else if (char === ' ') {\n timeouts.push(setTimeout(() => {\n \n }, delay))\n delay += ditMaxTime*2\n } else if (char === '/') {\n timeouts.push(setTimeout(() => {\n \n }, delay))\n delay += ditMaxTime*6\n }\n }\n }\n\n return { playMorseWord, play }\n})","import React, {useContext} from \"react\"\nimport { WPMContext } from \"../contexts/wpmContext\";\nimport useMorsePlayer from \"../hooks/useMorsePlayer\";\n\nexport default React.memo(function WordsPerMinute() {\n\n const {wpm, setWPM} = useContext(WPMContext)\n const {playMorseWord} = useMorsePlayer()\n\n const minWPM = 5\n const maxWPM = 30\n\n function handleChange(e) {\n if (Number(e.target.value) > maxWPM) {\n setWPM(maxWPM)\n } else if (Number(e.target.value) < minWPM) {\n setWPM(minWPM)\n } else {\n setWPM(Number(e.target.value))\n }\n }\n\n function increment() {\n setWPM(prevWPM => {\n if (prevWPM + 1 <= maxWPM) {\n return (prevWPM + 1)\n } else {\n return maxWPM\n }\n })\n }\n function decrement() {\n setWPM(prevWPM => {\n if (prevWPM - 1 >= minWPM) {\n return (prevWPM - 1)\n } else {\n return minWPM\n }\n })\n }\n \n return (\n \n
\n WPM ({minWPM}-{maxWPM}) \n
\n
\n \n \n \n Test playMorseWord('.....')}> \n
\n
\n )\n})","import React from \"react\"\n\nexport default React.memo(function MorseButtons() {\n\n return (\n <>\n \n \n \n
\n \n TAP BUTTON OR PRESS SPACEBAR\n
\n >\n )\n})","import React from \"react\"\nimport { contactLinks } from \"../data/social\"\n\nexport default (function Footer() {\n\n function handleClick(event) {\n window.open(contactLinks[event.target.id]['link'])\n }\n \n return (\n \n )\n})","import React, {useContext} from \"react\"\nimport { WordListPickerContext } from \"../contexts/wordListPickerContext\";\nimport { WordFeederContext } from \"../contexts/wordFeederContext\";\n\nexport default React.memo(function WordCountPicker() {\n\n const {setWordListCount, wordListCountMax} = useContext(WordListPickerContext)\n const {resetFeeder} = useContext(WordFeederContext)\n\n function handleChange(e) {\n resetFeeder()\n setWordListCount(e.target.value)\n }\n\n // Create Options for Select element\n let options = []\n for (let i = 0; i < wordListCountMax; i++) {\n options.push({i+1} )\n }\n \n return (\n \n
\n Challenge Word Count: (1-{wordListCountMax}) \n
\n
\n \n {options}\n \n
\n
\n )\n})","import React, {useContext} from \"react\"\nimport { WordListPickerContext } from \"../contexts/wordListPickerContext\";\nimport { WordFeederContext } from \"../contexts/wordFeederContext\";\nimport WordCountPicker from \"./WordCountPicker\";\n\nexport default React.memo(function WordListPicker() {\n\n const {wordListCategory, setWordListCategory, metadata} = useContext(WordListPickerContext)\n const {resetFeeder, setOrder} = useContext(WordFeederContext)\n\n const orderOpts = ['sequential', 'random']\n\n function handleClick(e) {\n resetFeeder()\n\n // Handle Word List Order selection\n if (orderOpts.includes(e.target.id)) {\n let buttons = document.querySelector(\".mode-picker#wordOrderPicker #buttons\").childNodes\n buttons.forEach(button => {\n if (button.id === e.target.id) {\n button.classList.add('selected')\n } else { button.classList.remove('selected')}\n })\n setOrder(e.target.id)\n }\n // Handle Word List Category selection\n else {\n setWordListCategory(e.target.value)\n }\n }\n\n let wordLists = Object.keys(metadata)\n // Create Option elements for Select element\n let options = wordLists.map((wl, index) => ({metadata[wl]['name']} ))\n\n return (\n \n
\n
\n Word List:\n
\n
\n \n {options}\n \n
\n
\n\n
\n
\n Word Order:\n
\n
\n \n Sequential\n \n \n Random\n \n
\n
\n\n
\n\n
\n
\n Description:\n
\n
\n {metadata[wordListCategory]['description']}\n
\n
\n
\n )\n})","import React, { useContext } from \"react\"\nimport { ChallengeContext } from \"../contexts/challengeContext\"\nimport WordListPicker from \"./WordListPicker\"\n\nexport default (function ChallengeReady() {\n\n const {startChallenge} = useContext(ChallengeContext)\n\n return (\n \n Challenge Options \n \n Start Challenge \n
\n )\n})\n\n","import React, { useContext } from \"react\"\nimport { GameClockContext } from \"../contexts/gameClockContext\"\nimport { ChallengeContext } from \"../contexts/challengeContext\"\nimport { WordListPickerContext } from \"../contexts/wordListPickerContext\"\n\nexport default (function ChallengeComplete() {\n\n const {gameClockTime} = useContext(GameClockContext)\n const {setChallengeState} = useContext(ChallengeContext)\n const {wordListCount, wordListCategory, metadata} = useContext(WordListPickerContext)\n\n function _continue() {\n setChallengeState('ready')\n }\n\n const minutes = Math.floor(gameClockTime / 60)\n const seconds = gameClockTime % 60\n\n let time = (minutes === 0) ? `${seconds} seconds` : `${minutes} minutes and ${seconds} seconds`\n\n return (\n \n Challenge Complete \n You completed {wordListCount} words \n from the {metadata[wordListCategory]['name']} word list \n in {time} ! \n Continue \n\n
\n )\n})\n\n","import React, { useContext } from \"react\"\nimport { ChallengeContext } from \"../contexts/challengeContext\"\nimport ChallengeReady from \"./ChallengeReady\"\nimport ChallengeComplete from \"./ChallengeComplete\"\n\nexport default (function ChallengeOverlay() {\n\n const {challengeState, setChallengeState} = useContext(ChallengeContext)\n \n return (\n \n {challengeState === 'ready' && }\n {challengeState === 'completed' && }\n
\n )\n})","import React from \"react\"\nimport useMorsePlayer from \"../hooks/useMorsePlayer\"\nimport straight_key from \"../media/images/straight_key.jpg\"\nimport electronic_key from \"../media/images/electronic_key.jpg\"\n\nexport default React.memo(function Info() {\n\n const {playMorseWord} = useMorsePlayer()\n\n return (\n \n
Morse Code \n
Morse code is a method of communication that uses short tones (dits) and long tones (dahs) in various sequences to make letters, numbers, and special characters. This tool will help beginners learn Morse code.
\n\n
Dits and Dahs \n
\n Dit playMorseWord('.')}> (. ) Short tones and the base unit length of Morse code communication. \n Dah playMorseWord('-')}> (- ) Long tones, each the length of three dits.\n
\n\n
Spacing \n
The spacing between dits and dahs matters in Morse code. Spacing of various lengths signify different things. \n\n Intra-character Spacing A letter in Morse code can be made up of multiple dits and dahs. The spaces between the dits and dahs that make up a single letter are each the length of one dit. E.g., three dits, each separated by one-dit-long spaces, is an \"S\". (... ) playMorseWord('...')}> \n\n Inter-character Spacing The space between consecutive letters is three dits long. E.g., three dits, each separated by a three-dit-long spaces is \"EEE\". (. . . ) playMorseWord('. . .')}> \n \n Inter-word Spacing The space between words is seven dits long. E.g., three dits, each separated by seven-dit-long spaces (denoted by a forward slash in this example: ././. ), is \"E E E\". playMorseWord('././.')}> \n
\n\n
Speed \n
\n The rate of communication is increased or decreased by adjusting the length of the dits, which in turn adjusts the length of dahs and spaces. Adjust the WPM (Words Per Minute) in the Options section to adjust the speed.\n
\n\n
Telegraph Key Types \n
The instrument used to send Morse code is called the key.
\n \n
\n
Straight Keys use a single button and generate tones when pressed down. Straight keys require greater accuracy as the length of dits, dahs, and spacing is completely under manual control.
\n
\n
Electronic Keys use paddles that automatically generate dits and dahs when pressed. The Electronic Keyer used here is an Iambic keyer that uses two paddles–left paddle for dits, right paddle for dahs. Pressing both paddles simultaneously automatically alternates between dit and dah. Mode B automatically adds an extra alternate dit or dah. Switch between the two paddles at the appropriate times to build letters in Morse code.
\n\n
Check out this video for a demonstration of the difference between Straight and Electronic keys.
\n
\n )\n})","/* eslint-disable array-callback-return */\nimport React from \"react\"\nimport morseCode from '../data/morse-code.json'\nimport useMorsePlayer from \"../hooks/useMorsePlayer\";\n\nexport default React.memo(function Legend() {\n\n const { playMorseWord } = useMorsePlayer()\n\n function handleClick(e) {\n e.preventDefault()\n\n let word = e.target.innerText\n\n if (e.target.className === 'alpha') {\n word = convertWordToMorse(word)\n }\n playMorseWord(word)\n }\n\n function convertWordToMorse(word) {\n let morse = ''\n for (let i in word) {\n morse += morseCode[word[i].toLowerCase()]\n morse += ' '\n }\n return morse\n }\n\n const numbers = Object.keys(morseCode).map((morse, index) =>\n {\n if (index < 10) {\n return (\n \n {morse.toUpperCase()} \n {morseCode[morse]} \n \n )\n }\n }\n )\n const letters = Object.keys(morseCode).map((morse, index) =>\n {\n if (index >= 10 && index < 36) {\n return (\n \n {morse.toUpperCase()} \n {morseCode[morse]} \n \n )\n }\n }\n )\n const special = Object.keys(morseCode).map((morse, index) =>\n {\n if (index > 36) {\n return (\n \n {morse.toUpperCase()} \n {morseCode[morse]} \n \n )\n }\n }\n )\n\n return (\n \n
\n Legend\n
\n
\n {letters}\n {numbers}\n {special}\n
\n
\n )\n})","import React, { useState, useEffect } from \"react\"\nimport morseCode from '../data/morse-code.json'\nimport useMorsePlayer from \"../hooks/useMorsePlayer\";\n\nexport default (function PlayMorseInput() {\n\n const { playMorseWord } = useMorsePlayer()\n const [inputValue, setInputValue] = useState('')\n const [morseTranslation, setMorseTranslation] = useState('')\n\n function handleChange(e) {\n e.preventDefault()\n setInputValue(e.target.value)\n }\n\n function handlePlay() {\n playMorseWord(morseTranslation)\n }\n\n function convertWordToMorse(word) {\n let morse = ''\n for (let i in word) {\n morse += morseCode[word[i].toLowerCase()]\n }\n return morse\n }\n\n // Live translation of text into morse code\n useEffect(() => {\n let arr = Array.from(inputValue.trim().toLowerCase())\n let morse = arr.map(item => {\n if (item === ' ') {\n return '/'\n } else {\n let r = convertWordToMorse(item)\n return (r === 'undefined' ? '?' : r + ' ')\n }\n })\n let a = morse.map(i => i.trim()).join(' ').replace(/ \\/ /g,'/').replace(/ \\?/g,'?')\n setMorseTranslation(a)\n\n }, [inputValue])\n\n return (\n \n )\n})","import React, { useState } from \"react\"\nimport Info from \"./Info\"\nimport Legend from \"./Legend\"\nimport PlayMorseInput from \"./PlayMorseInput\"\n\nexport default (function SidebarLeft() {\n\n const [sidebarContent, setSidebarContent] = useState('nav-learn')\n\n // Hide/show sidebar\n function toggleLeft() {\n document.querySelector('.sidebar#left').classList.toggle('hide')\n document.querySelector('#main-interface').classList.toggle('expandLeft')\n }\n\n // Handle sidebar navigation selection\n function navClicked(e) {\n if (e.target.id === 'nav-learn') {\n setSidebarContent('nav-learn')\n } else if (e.target.id === 'nav-legend') {\n setSidebarContent('nav-legend')\n } else {\n setSidebarContent('nav-play')\n }\n \n let navItems = document.querySelector(\".navbar\").childNodes\n navItems.forEach(item => {\n if (item.id === e.target.id) {\n item.classList.add('selected')\n } else {\n item.classList.remove('selected')\n }\n })\n }\n\n return (\n \n \n
\n )\n\n})\n","import React, {useContext} from \"react\"\nimport { FrequencyContext } from \"../contexts/frequencyContext\";\n\nexport default React.memo(function FrequencyPicker(props) {\n\n const {frequency, setFrequency} = useContext(FrequencyContext)\n\n const minFreq = 300\n const maxFreq = 1500\n\n function handleChange(e) {\n if (Number(e.target.value) > maxFreq) {\n setFrequency(maxFreq)\n } else if (Number(e.target.value) < minFreq) {\n setFrequency(minFreq)\n } else {\n setFrequency(Number(e.target.value))\n }\n }\n\n function increment() {\n setFrequency(prevFreq => {\n if (prevFreq + 10 <= maxFreq) {\n return (prevFreq + 10)\n } else {\n return maxFreq\n }\n })\n }\n\n function decrement() {\n setFrequency(prevFreq => {\n if (prevFreq - 10 >= minFreq) {\n return (prevFreq - 10)\n } else {\n return minFreq\n }\n })\n }\n \n return (\n \n
\n Frequency ({minFreq}-{maxFreq}) \n
\n
\n \n \n \n
\n
\n )\n})","import React, {useContext} from 'react';\nimport './css/App.css';\n\nimport { GameModeContext } from \"./contexts/gameModeContext\"\nimport { MorseBufferContextProvider } from \"./contexts/morseBufferContext\"\nimport { WordFeederContextProvider } from './contexts/wordFeederContext';\nimport { WordListPickerContextProvider } from './contexts/wordListPickerContext';\nimport { GameClockContextProvider } from './contexts/gameClockContext';\nimport { WPMContextProvider } from './contexts/wpmContext';\nimport { FrequencyContextProvider } from './contexts/frequencyContext';\nimport { KeyTypeContextProvider } from './contexts/keyTypeContext';\nimport { ChallengeContextProvider } from './contexts/challengeContext';\n\nimport PracticeMode from './app-modes/PracticeMode';\nimport ChallengeMode from './app-modes/ChallengeMode'\n\nimport ModePicker from './components/ModePicker'\nimport KeyTypePicker from './components/KeyTypePicker'\nimport Header from './components/Header';\nimport WordsPerMinute from \"./components/WordsPerMinute\"\nimport MorseButtons from './components/MorseButtons'\nimport Footer from './components/Footer';\nimport ChallengeOverlay from './components/ChallengeOverlay';\nimport SidebarLeft from './components/SidebarLeft';\nimport FrequencyPicker from './components/FrequencyPicker';\n\nexport default React.memo(function App() {\n\n const {gameMode} = useContext(GameModeContext)\n\n return (\n <>\n \n \n
\n \n \n \n \n \n \n \n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n {gameMode === 'practice' &&
}\n {gameMode === 'challenge' &&\n <>\n
\n
\n >\n }\n
\n
\n \n \n \n \n \n \n \n \n
\n \n >\n );\n})","// This optional code is used to register a service worker.\n// register() is not called by default.\n\n// This lets the app load faster on subsequent visits in production, and gives\n// it offline capabilities. However, it also means that developers (and users)\n// will only see deployed updates on subsequent visits to a page, after all the\n// existing tabs open on the page have been closed, since previously cached\n// resources are updated in the background.\n\n// To learn more about the benefits of this model and instructions on how to\n// opt-in, read https://bit.ly/CRA-PWA\n\nconst isLocalhost = Boolean(\n window.location.hostname === 'localhost' ||\n // [::1] is the IPv6 localhost address.\n window.location.hostname === '[::1]' ||\n // 127.0.0.0/8 are considered localhost for IPv4.\n window.location.hostname.match(\n /^127(?:\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/\n )\n);\n\nexport function register(config) {\n if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {\n // The URL constructor is available in all browsers that support SW.\n const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);\n if (publicUrl.origin !== window.location.origin) {\n // Our service worker won't work if PUBLIC_URL is on a different origin\n // from what our page is served on. This might happen if a CDN is used to\n // serve assets; see https://github.com/facebook/create-react-app/issues/2374\n return;\n }\n\n window.addEventListener('load', () => {\n const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;\n\n if (isLocalhost) {\n // This is running on localhost. Let's check if a service worker still exists or not.\n checkValidServiceWorker(swUrl, config);\n\n // Add some additional logging to localhost, pointing developers to the\n // service worker/PWA documentation.\n navigator.serviceWorker.ready.then(() => {\n console.log(\n 'This web app is being served cache-first by a service ' +\n 'worker. To learn more, visit https://bit.ly/CRA-PWA'\n );\n });\n } else {\n // Is not localhost. Just register service worker\n registerValidSW(swUrl, config);\n }\n });\n }\n}\n\nfunction registerValidSW(swUrl, config) {\n navigator.serviceWorker\n .register(swUrl)\n .then(registration => {\n registration.onupdatefound = () => {\n const installingWorker = registration.installing;\n if (installingWorker == null) {\n return;\n }\n installingWorker.onstatechange = () => {\n if (installingWorker.state === 'installed') {\n if (navigator.serviceWorker.controller) {\n // At this point, the updated precached content has been fetched,\n // but the previous service worker will still serve the older\n // content until all client tabs are closed.\n console.log(\n 'New content is available and will be used when all ' +\n 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'\n );\n\n // Execute callback\n if (config && config.onUpdate) {\n config.onUpdate(registration);\n }\n } else {\n // At this point, everything has been precached.\n // It's the perfect time to display a\n // \"Content is cached for offline use.\" message.\n console.log('Content is cached for offline use.');\n\n // Execute callback\n if (config && config.onSuccess) {\n config.onSuccess(registration);\n }\n }\n }\n };\n };\n })\n .catch(error => {\n console.error('Error during service worker registration:', error);\n });\n}\n\nfunction checkValidServiceWorker(swUrl, config) {\n // Check if the service worker can be found. If it can't reload the page.\n fetch(swUrl, {\n headers: { 'Service-Worker': 'script' }\n })\n .then(response => {\n // Ensure service worker exists, and that we really are getting a JS file.\n const contentType = response.headers.get('content-type');\n if (\n response.status === 404 ||\n (contentType != null && contentType.indexOf('javascript') === -1)\n ) {\n // No service worker found. Probably a different app. Reload the page.\n navigator.serviceWorker.ready.then(registration => {\n registration.unregister().then(() => {\n window.location.reload();\n });\n });\n } else {\n // Service worker found. Proceed as normal.\n registerValidSW(swUrl, config);\n }\n })\n .catch(() => {\n console.log(\n 'No internet connection found. App is running in offline mode.'\n );\n });\n}\n\nexport function unregister() {\n if ('serviceWorker' in navigator) {\n navigator.serviceWorker.ready.then(registration => {\n registration.unregister();\n });\n }\n}\n","import React from 'react';\nimport ReactDOM from 'react-dom';\nimport './index.css';\nimport App from './App';\nimport * as serviceWorker from './serviceWorker';\nimport {GameModeContextProvider} from \"./contexts/gameModeContext\"\n\n\nReactDOM.render(\n \n \n \n\n , document.getElementById('root'));\n\n// If you want your app to work offline and load faster, you can change\n// unregister() to register() below. Note this comes with some pitfalls.\n// Learn more about service workers: https://bit.ly/CRA-PWA\nserviceWorker.unregister();\n"],"sourceRoot":""}
\ No newline at end of file
diff --git a/static/js/main.d1a1cd6f.chunk.js b/static/js/main.d1a1cd6f.chunk.js
deleted file mode 100644
index f69aadb..0000000
--- a/static/js/main.d1a1cd6f.chunk.js
+++ /dev/null
@@ -1,2 +0,0 @@
-(this["webpackJsonplearn-morse-code"]=this["webpackJsonplearn-morse-code"]||[]).push([[0],[,,function(e){e.exports=JSON.parse('{"-----":"0",".----":"1","..---":"2","...--":"3","....-":"4",".....":"5","-....":"6","--...":"7","---..":"8","----.":"9",".-":"a","-...":"b","-.-.":"c","-..":"d",".":"e","..-.":"f","--.":"g","....":"h","..":"i",".---":"j","-.-":"k",".-..":"l","--":"m","-.":"n","---":"o",".--.":"p","--.-":"q",".-.":"r","...":"s","-":"t","..-":"u","...-":"v",".--":"w","-..-":"x","-.--":"y","--..":"z",".-.-.-":".","--..--":",","..--..":"?","-.-.--":"!","-....-":"-","-..-.":"/",".--.-.":"@","-.--.":"(","-.--.-":")",".----.":"\'",".-..-.":"\\"",".-...":"&","---...":": ","-.-.-.":";","-...-":"=",".-.-.":"+","..--.-":"_","...-..-":"$"}')},function(e){e.exports=JSON.parse('{"mainVolume":0.2,"historySize":50}')},function(e){e.exports=JSON.parse('{"0":"-----","1":".----","2":"..---","3":"...--","4":"....-","5":".....","6":"-....","7":"--...","8":"---..","9":"----.","a":".-","b":"-...","c":"-.-.","d":"-..","e":".","f":"..-.","g":"--.","h":"....","i":"..","j":".---","k":"-.-","l":".-..","m":"--","n":"-.","o":"---","p":".--.","q":"--.-","r":".-.","s":"...","t":"-","u":"..-","v":"...-","w":".--","x":"-..-","y":"-.--","z":"--..",".":".-.-.-",",":"--..--","?":"..--..","!":"-.-.--","-":"-....-","/":"-..-.","@":".--.-.","(":"-.--.",")":"-.--.-","\'":".----.","\\"":".-..-.","&":".-...",":":"---...",";":"-.-.-.","=":"-...-","+":".-.-.","_":"..--.-","$":"...-..-"}')},,function(e,t,n){},,,function(e){e.exports=JSON.parse('{"words":["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"]}')},function(e){e.exports=JSON.parse('{"words":["1","2","3","4","5","6","7","8","9","0"]}')},function(e){e.exports=JSON.parse('{"words":["the","be","to","of","and","a","in","that","have","I","it","for","not","on","with","he","as","you","do","at","this","but","his","by","from","they","we","say","her","she","or","an","will","my","one","all","would","there","their","what","so","up","out","if","about","who","get","which","go","me","when","make","can","like","time","no","just","him","know","take","person","into","year","your","good","some","could","them","see","other","than","then","now","look","only","come","its","over","think","also","back","after","use","two","how","our","work","first","well","way","even","new","want","because","any","these","give","day","most","us"]}')},function(e){e.exports=JSON.parse('{"words":["Liam","Noah","Elijah","Logan","Mason","James","Aiden","Ethan","Lucas","Jacob","Michael","Matthew","Benjamin","Alexander","William","Daniel","Jayden","Oliver","Carter","Sebastian","Joseph","David","Gabriel","Julian","Jackson","Anthony","Dylan","Wyatt","Grayson","Isaiah","Christopher","Joshua","Christian","Andrew","Samuel","Mateo","Jaxon","Josiah","John","Luke","Ryan","Nathan","Isaac","Owen","Henry","Levi","Aaron","Caleb","Jeremiah","Landon"]}')},function(e){e.exports=JSON.parse('{"words":["Emma","Olivia","Ava","Isabella","Sophia","Mia","Amelia","Charlotte","Abigail","Emily","Harper","Evelyn","Madison","Victoria","Sofia","Scarlett","Aria","Elizabeth","Camila","Layla","Ella","Chloe","Zoey","Penelope","Skylar","Grace","Mila","Lillian","Aaliyah","Lily","Paisley","Bella","Brooklyn","Savannah","Luna","Natalie","Ellie","Leah","Audrey","Ariana","Aurora","Zoe","Hannah","Violet","Samantha","Nora","Nevaeh","Serenity","Gabriella","Hailey"]}')},function(e){e.exports=JSON.parse('{"words":["dilithium","borg","replicator","picard","vulcan","ensign","phaser","kirk","warbird","ferengi","hypospray","tribble","sisko","starfleet","engage","holodeck","warp","enterprise","romulan","quadrant","tricorder","georgiou","futile","klingon","janeway","delta","assimilate","energize"]}')},function(e,t,n){e.exports=n.p+"static/media/straight_key.f140bc7b.jpg"},function(e,t,n){e.exports=n.p+"static/media/electronic_key.cd2cf560.jpg"},function(e,t,n){e.exports=n(23)},,,,,function(e,t,n){},function(e,t,n){"use strict";n.r(t);var a=n(0),r=n.n(a),o=n(8),i=n.n(o),l=(n(22),n(6),n(1)),c=r.a.createContext();var s=r.a.createContext();function d(e){var t=Object(a.useState)(""),n=Object(l.a)(t,2),o=n[0],i=n[1],c=Object(a.useState)([]),d=Object(l.a)(c,2),u=d[0],m=d[1];return r.a.createElement(s.Provider,{value:{morseCharBuffer:o,morseWords:u,setMorseCharBuffer:i,setMorseWords:m}},e.children)}var u=n(5),m=n(9),h=n(10),p=n(11),f=n(12),v=n(13),E=n(14),g=r.a.createContext();function y(e){var t=Object(a.useState)("alphabet"),n=Object(l.a)(t,2),o=n[0],i=n[1],c=Object(a.useState)(10),s=Object(l.a)(c,2),d=s[0],y=s[1],b=[];"alphabet"===o?b=m.words:"numbers"===o?b=h.words:"boys"===o?b=f.words:"girls"===o?b=v.words:"startrek"===o?b=E.words:"common100"===o&&(b=p.words);var w=b.length;return r.a.createElement(g.Provider,{value:{wordList:b.slice(0,d),wordListShuffled:function(e){for(var t,n,a=Object(u.a)(e),r=a.length;0!==r;)n=Math.floor(Math.random()*r),t=a[r-=1],a[r]=a[n],a[n]=t;return a}(b).slice(0,d),wordListCategory:o,setWordListCategory:i,metadata:{alphabet:{name:"Alphabet",description:"All letters of the alphabet"},numbers:{name:"Numbers",description:"0-9"},boys:{name:"Boys Names",description:"Popular Boys Names"},girls:{name:"Girls Names",description:"Popular Girls Names"},startrek:{name:"Star Trek",description:"Star Trek universe"},common100:{name:"Common Words",description:"100 Most Common Words"}},wordListCount:d,setWordListCount:y,wordListCountMax:w}},e.children)}var b=r.a.createContext();function w(e){var t,n=Object(a.useContext)(g),o=n.wordList,i=n.wordListShuffled,c=Object(a.useState)(0),s=Object(l.a)(c,2),d=s[0],u=s[1],m=Object(a.useState)("sequential"),h=Object(l.a)(m,2),p=h[0],f=h[1];return"sequential"===p?t=void 0===o[d]?[o[0]]:o[d]:"random"===p&&(t=void 0===i[d]?[i[0]]:i[d]),r.a.createElement(b.Provider,{value:{word:t,getNextWord:function(){u((function(e){return e+1}))},resetFeeder:function(){u(0)},setOrder:f}},e.children)}var C=n(2),k=r.a.createContext();function O(e){var t,n=Object(a.useState)("ready"),o=Object(l.a)(n,2),i=o[0],c=o[1],d=Object(a.useContext)(b).resetFeeder,u=Object(a.useContext)(b),m=u.word,h=u.getNextWord,p=Object(a.useContext)(s),f=p.morseCharBuffer,v=p.setMorseCharBuffer,E=f.split("_").filter((function(e){return""!==e})),g="",y=[],w=[],O=0;function j(){"completed"!==i&&(c("completed"),d(),L())}function L(){var e=document.getElementById("challenge-overlay");e.classList.remove("fade"),e.classList.remove("hide")}return"object"===typeof m?(j(),t=m[0].split("")):t=m.split(""),E.forEach((function(e,n){"_"===f.slice(-1)&&((C[E[n]]||"[?]")===t[n-O].toLowerCase()?(y.push(n-O),document.getElementById("challengeWord").childNodes[n-O].classList.add("correct")):(w.push(n),w.length>0&&(v((function(e){var t=e.split("_").filter((function(e){return""!==e}));return t.splice(w[0],1),t=t.join("_")+"_"})),w.splice(1,w.length)),O=w.length))})),y.length===t.length&&(g="correct",setTimeout((function(){v(""),E=[],w=[],O=0,null!==document.getElementById("challengeWord")&&document.getElementById("challengeWord").childNodes.forEach((function(e){e.classList="cLetter"}))}),800),setTimeout((function(){y.length>0&&(y=[],h())}),1e3)),r.a.createElement(k.Provider,{value:{challengeState:i,setChallengeState:c,startChallenge:function(){var e,t=3;document.getElementById("challengeReady").classList.add("starting"),document.getElementById("challengeReady").innerHTML='Challenge starting in '.concat(t," "),e=setInterval((function(){0===--t&&(document.getElementById("challenge-overlay").classList.add("fade"),clearInterval(e),setTimeout((function(){document.getElementById("challenge-overlay").classList.add("hide"),c("started")}),900)),document.getElementById("challengeReady").innerHTML='Challenge starting in '.concat(t," ")}),1e3)},completeChallenge:j,cancelChallenge:function(){"cancelled"!==i&&(c("cancelled"),d(),L())},challengeWordClass:g,morseArray:E,incorrectMorseIndexes:w}},e.children)}var j=r.a.createContext();function L(e){var t=Object(a.useState)(0),n=Object(l.a)(t,2),o=n[0],i=n[1],c=Object(a.useState)(!1),s=Object(l.a)(c,2),d=s[0],m=s[1],h=Object(a.useState)([]),p=Object(l.a)(h,2),f=p[0],v=p[1],E=Object(a.useContext)(k),g=E.challengeState,y=E.setChallengeState;function b(){d||(m(!0),v((function(e){return[].concat(Object(u.a)(e),[setInterval((function(){null!==document.getElementById("gameClock")?i((function(e){return e+1})):w()}),1e3)])})))}function w(){d&&(C(),m(!1))}function C(){for(var e=0;e=v&&j=E?(i((function(e){return e+"/"})),clearInterval(L),L=0,j=0):"challenge"===h&&j>=v&&(i((function(e){return e+"_"})),clearInterval(L),L=0,j=0)}),1),a()}),t))):setTimeout((function(){a()}),3*f)}))}function B(){y&&b?"left"===k?(C.push("-"),k=null):(C.push("."),C.push("-")):y&&!b?C.push("."):b&&!y&&C.push("-"),C.length>0&&function(){var e=C,t=0;for(var n in e)"."===e[n]?t+=2*f:"-"===e[n]&&(t+=4*f);function a(){w=!1,C=[],B()}var r=setTimeout((function(){a()}),t);w=!0;for(var o=function(t){N&&(e.pop(),clearTimeout(r),a()),T=T.then((function(){return A(e[t])}))},i=0;i20&&(O=!1,clearInterval(t),e=0)}),1)}function _(){O=!1,clearInterval(t),e<10&&(N=!0,C.pop()),e=0}window.AudioContext=window.AudioContext||window.webkitAudioContext,n=M?new M:null,Object(a.useEffect)((function(){return document.addEventListener("keydown",q),document.addEventListener("keyup",W),document.querySelectorAll(".paddle").forEach((function(e){e.addEventListener("mousedown",q),e.addEventListener("touchstart",q),e.addEventListener("mouseout",W),e.addEventListener("mouseup",W),e.addEventListener("touchend",W)})),function(){document.removeEventListener("keydown",q),document.removeEventListener("keyup",W),document.querySelectorAll(".paddle").forEach((function(e){e.removeEventListener("mousedown",q),e.removeEventListener("touchstart",q),e.removeEventListener("mouseout",W),e.removeEventListener("mouseup",W),e.removeEventListener("touchend",W)})),clearInterval(t),clearInterval(L)}}),[m,h,p]),Object(a.useEffect)((function(){if("/"===o.slice(-1)&&"practice"===h){var e=o.slice(0,o.length-1);d((function(t){return[e].concat(Object(u.a)(t))})),l.length>=g&&d((function(e){return e.slice(0,e.length-1)})),i("")}}),[o])},q=r.a.memo((function(){B()})),W=function(e){return" "===e.dd?r.a.createElement("div",{className:"ditDah"},"\xa0"):r.a.createElement("div",{className:"ditDah"},e.dd)},P=r.a.memo((function(){var e=Object(a.useContext)(s).morseCharBuffer,t=e.split("").map((function(e,t){return r.a.createElement(W,{key:t,dd:e})})),n="",o=e.split(" ");if(""===e);else for(var i in o)" "===o[i]?n+=" ":void 0===C[o[i]]?n+=""===o[i]?"":"[?]":n+=C[o[i]];return r.a.createElement("div",{id:"morseBufferDisplay"},r.a.createElement("div",{id:"overlay"}),r.a.createElement("div",{id:"ditDahs-container"},r.a.createElement("div",{id:"ditDahs"},t)),r.a.createElement("div",{id:"alphanumeric-container"},r.a.createElement("div",{id:"alphanumeric"},n.toUpperCase())))})),_=function(){var e=Object(a.useContext)(s),t=e.morseWords,n=e.setMorseWords,o="";return t.forEach((function(e,t){if(e.includes(" ")){var n="";e.split(" ").forEach((function(e){void 0===C[e]?n+="[?]":n+=C[e].toUpperCase()})),o=n+" "+o}else o=void 0===C[e]?"[?] "+o:C[e].toUpperCase()+" "+o})),r.a.createElement("div",{id:"morse-history"},r.a.createElement("div",{id:"morseHistory-textbox"},o),r.a.createElement("div",{id:"clear"},r.a.createElement("span",{id:"message"},'"',r.a.createElement("span",{className:"ditDah"},"[?]"),'" signifies no translation available.'),r.a.createElement("button",{id:"clear-history",onClick:function(){n([])}},"Clear")))},D=function(){var e,t,n,r=Object(a.useContext)(s),o=r.morseCharBuffer,i=r.setMorseCharBuffer,l=r.morseWords,d=r.setMorseWords,m=Object(a.useContext)(x).wpm,h=Object(a.useContext)(c).gameMode,p=Object(a.useContext)(S).frequency,f=0,v=0,E=0,g=0,y=1200/m*.3,b=3*y,w=7*y,C=I.historySize,k=window.AudioContext||window.webkitAudioContext||!1;window.AudioContext=window.AudioContext||window.webkitAudioContext,e=k?new k:null;var O=!1;function j(a){if("touchstart"===a.type&&a.preventDefault(),32===a.keyCode){if("morseInput"===document.activeElement.id)return;"body"!==document.activeElement.tagName.toLowerCase()&&(a.preventDefault(),document.activeElement.blur())}O||(O=!0,32!==a.keyCode&&"morseButton"!==a.target.id&&"paddle"!==a.target.className||a.repeat||(document.getElementById("morseButton").classList.add("active"),"interrupted"===e.state&&e.resume(),(t=e.createOscillator()).frequency.value=p,t.type="sine",(n=e.createGain()).gain.exponentialRampToValueAtTime(I.mainVolume,e.currentTime),t.connect(n),n.connect(e.destination),t.start(),g>=b&&g=w?(i((function(e){return e+"/"})),clearInterval(E),E=0,g=0):"challenge"===h&&g>=b&&(i((function(e){return e+"_"})),clearInterval(E),E=0,g=0)}),1),void 0!==t&&"running"===t.context.state&&(n.gain.setTargetAtTime(1e-4,e.currentTime,.001),t.stop(e.currentTime+.05))))}Object(a.useEffect)((function(){document.addEventListener("keydown",j),document.addEventListener("keyup",L);var e=document.getElementById("morseButton");return e.addEventListener("touchstart",j),e.addEventListener("touchend",L),e.addEventListener("mousedown",j),e.addEventListener("mouseout",L),e.addEventListener("mouseup",L),function(){document.removeEventListener("keydown",j),document.removeEventListener("keyup",L);var e=document.getElementById("morseButton");e.removeEventListener("touchstart",j),e.removeEventListener("touchend",L),e.removeEventListener("mousedown",j),e.removeEventListener("mouseout",L),e.removeEventListener("mouseup",L),clearInterval(f),clearInterval(E)}}),[m,h,p]),Object(a.useEffect)((function(){if("/"===o.slice(-1)&&"practice"===h){var e=o.slice(0,o.length-1);d((function(t){return[e].concat(Object(u.a)(t))})),l.length>=C&&d((function(e){return e.slice(0,e.length-1)})),i("");var t=document.getElementById("morseHistory-textbox");t.scrollTop=t.scrollHeight}}),[o])},J=r.a.memo((function(){D()})),R=function(){var e=Object(a.useContext)(M).keyType;return r.a.createElement(r.a.Fragment,null,"straight"===e?r.a.createElement(J,null):r.a.createElement(q,null),r.a.createElement(P,null),r.a.createElement("br",null),r.a.createElement(_,null))},G=r.a.memo((function(e){var t=e.morseArray,n=[];for(var a in t){var o=t[a];n.push(r.a.createElement("span",{key:a},o))}return r.a.createElement("div",{id:"challengeBufferDisplay"},r.a.createElement("div",{id:"ditDahs"},n))})),H=function(e){return r.a.createElement("div",{id:"challengeControls"},r.a.createElement("button",{onClick:e.cancelChallenge},"Exit Challenge"))},F=r.a.memo((function(e){var t=Object(a.useContext)(b).word,n=("object"===typeof t?t[0].split(""):t.split("")).map((function(e,t){return r.a.createElement("span",{key:t,className:"cLetter"},e)}));return r.a.createElement("div",{id:"challengeWord",className:e.challengeWordClass},n)})),V=function(e){var t=Object(a.useContext)(j).gameClockTime,n=Math.floor(t/60),o=t%60;return r.a.createElement("div",{id:"gameClock"},"Time Elapsed: ",r.a.createElement("span",{id:"clockTime"},n," minutes ",o," seconds"))},U=r.a.memo((function(){var e=Object(a.useContext)(M).keyType,t=Object(a.useContext)(k),n=t.challengeState,o=t.cancelChallenge,i=t.morseArray,l=t.challengeWordClass;return r.a.createElement(r.a.Fragment,null,"started"===n?"straight"===e?r.a.createElement(J,null):r.a.createElement(q,null):r.a.createElement(r.a.Fragment,null),r.a.createElement("div",{id:"challenge-header"},r.a.createElement(V,null),r.a.createElement(H,{cancelChallenge:o})),r.a.createElement(F,{challengeWordClass:l}),r.a.createElement(G,{morseArray:i}))})),K=r.a.memo((function(){var e=Object(a.useContext)(c).setGameMode,t=Object(a.useContext)(s).setMorseCharBuffer,n=Object(a.useContext)(b).resetFeeder,o=Object(a.useContext)(j),i=o.stopGameClock,l=o.setGameClockTime,d=o.clockIsRunning,u=Object(a.useContext)(k).setChallengeState;function m(a){t(""),n(),u("ready"),e(a.target.id),d&&(i(),l(0)),document.querySelector(".mode-picker#gameMode #buttons").childNodes.forEach((function(e){e.id===a.target.id?e.classList.add("selected"):e.classList.remove("selected")}))}return r.a.createElement("div",{id:"gameMode",className:"mode-picker"},r.a.createElement("div",{id:"title"},"Mode"),r.a.createElement("div",{id:"buttons"},r.a.createElement("button",{id:"practice",className:"selected",onClick:m},"Practice Mode"),r.a.createElement("button",{id:"challenge",onClick:m},"Challenge Mode")))})),z=r.a.memo((function(){var e=Object(a.useContext)(M),t=e.setKeyType,n=e.keyType;function o(e){t(e.target.id),document.querySelector(".mode-picker#keyType #buttons").childNodes.forEach((function(t){t.id===e.target.id?t.classList.add("selected"):t.classList.remove("selected")})),"electronic"===e.target.id?(document.querySelector("#morseButton").classList.add("showPaddles"),document.querySelector(".paddle").classList.add("showPaddles"),document.querySelector(".paddle#left").classList.add("showPaddles"),document.querySelector(".paddle#right").classList.add("showPaddles"),document.getElementById("morseButtonText").innerHTML="TAP/HOLD BUTTONS OR PRESS COMMA / PERIOD"):(document.querySelector("#morseButton").classList.remove("showPaddles"),document.querySelector(".paddle").classList.remove("showPaddles"),document.querySelector(".paddle#left").classList.remove("showPaddles"),document.querySelector(".paddle#right").classList.remove("showPaddles"),document.getElementById("morseButtonText").innerHTML="TAP BUTTON OR PRESS SPACEBAR")}return Object(a.useEffect)((function(){document.querySelector("button#".concat(n)).classList.add("selected")}),[]),r.a.createElement("div",{id:"keyType",className:"mode-picker"},r.a.createElement("div",{id:"title"},"Key Type"),r.a.createElement("div",{id:"buttons"},r.a.createElement("button",{id:"straight",onClick:o},"Straight Key"),r.a.createElement("button",{id:"electronic",onClick:o},"Electronic Key")))})),$={twitter:{name:"Twitter",icon:"ri-twitter-fill",link:"https://twitter.com/intent/tweet?text=Check%20out%20this%20site%20that%20helps%20you%20learn%20Morse%20Code%3A%20https%3A//learnmorsecode.net%20%40genemecija%20%23morse%20%23morsecode"},facebook:{name:"Facebook",icon:"ri-facebook-box-fill",link:"https://www.facebook.com/sharer/sharer.php?u=https%3A//learnmorsecode.net"},email:{name:"Email",icon:"ri-mail-line",link:"mailto:?subject="+encodeURIComponent("Check out this site that helps you learn Morse code! https://learnmorsecode.net")}},Z={email:{name:"Email",icon:"ri-mail-line",link:"mailto:gene@genemecija.com?subject="+encodeURIComponent("Hello, Gene!")},github:{name:"GitHub",icon:"ri-github-fill",link:"https://github.com/genemecija/learn-morse-code/"},twitter:{name:"Twitter",icon:"ri-twitter-fill",link:"https://twitter.com/genemecija"}},Y=function(){function e(e){var t=e.target.id,n="900",a="500";"email"===t&&(n="150",a="150"),function(e,t,n,a){var r=void 0!==window.screenLeft?window.screenLeft:window.screen.left,o=void 0!==window.screenTop?window.screenTop:window.screen.top,i=(window.innerWidth?window.innerWidth:document.documentElement.clientWidth?document.documentElement.clientWidth:window.screen.width)/2-n/2+r,l=(window.innerHeight?window.innerHeight:document.documentElement.clientHeight?document.documentElement.clientHeight:window.screen.height)/2-a/2+o,c=window.open(e,t,"scrollbars=yes, width="+n+", height="+a+", top="+l+", left="+i);window.focus&&c.focus()}($[t].link,"Share",n,a)}var t=Object.keys($).map((function(t,n){return r.a.createElement("i",{id:t,key:n,onClick:e,className:$[t].icon})}));return r.a.createElement("div",{id:"header"},r.a.createElement("div",{id:"title"},"Learn Morse Code"),r.a.createElement("div",{id:"social-links"},"Share: ",r.a.createElement("span",{id:"share-icons"},t)))},Q=function(){var e,t=Object(a.useContext)(x).wpm,n=Object(a.useContext)(S).frequency,r=1200/t,o=window.AudioContext||window.webkitAudioContext;function i(t){var a,o="."===t?r:3*r;"interrupted"===e.state&&e.resume(),(a=e.createOscillator()).frequency.value=n,a.type="sine";var i=e.currentTime,l=e.createGain();l.gain.exponentialRampToValueAtTime(I.mainVolume,i),l.gain.setValueAtTime(I.mainVolume,i),a.connect(l),l.connect(e.destination),a.start(i),setTimeout((function(){l.gain.setTargetAtTime(1e-4,e.currentTime,.009),a.stop(e.currentTime+.05)}),o)}window.AudioContext=window.AudioContext||window.webkitAudioContext,e=o?new o:null;var l=[],c=[];return{playMorseWord:function(e){l=[];for(var t=0;t=i?e-1:i}))}},r.a.createElement("i",{className:"ri-arrow-down-s-line"})),r.a.createElement("input",{type:"number",name:"wpm",id:"wpm-input",min:"5",max:"30",value:t,onChange:function(e){Number(e.target.value)>l?n(l):Number(e.target.value)=10&&n<36)return r.a.createElement("button",{key:"legend_item_"+n,className:"item",onClick:t},r.a.createElement("span",{className:"alpha",key:"legend_btn_"+n},e.toUpperCase()),r.a.createElement("span",{className:"morse",key:"legend_spn_"+n},me[e]))})),o=Object.keys(me).map((function(e,n){if(n>36)return r.a.createElement("button",{key:"legend_item_"+n,className:"item",onClick:t},r.a.createElement("span",{className:"alpha",key:"legend_btn_"+n},e.toUpperCase()),r.a.createElement("span",{className:"morse",key:"legend_spn_"+n},me[e]))}));return r.a.createElement("div",{id:"legend"},r.a.createElement("div",{id:"legend-title"},"Legend"),r.a.createElement("div",{id:"legend-items"},a,n,o))})),pe=function(){var e=Q().playMorseWord,t=Object(a.useState)(""),n=Object(l.a)(t,2),o=n[0],i=n[1],c=Object(a.useState)(""),s=Object(l.a)(c,2),d=s[0],u=s[1];return Object(a.useEffect)((function(){var e=Array.from(o.trim().toLowerCase()).map((function(e){if(" "===e)return"/";var t=function(e){var t="";for(var n in e)t+=me[e[n].toLowerCase()];return t}(e);return"undefined"===t?"?":t+" "})).map((function(e){return e.trim()})).join(" ").replace(/ \/ /g,"/").replace(/ \?/g,"?");u(e)}),[o]),r.a.createElement("div",{id:"playMorseInput"},r.a.createElement("div",{id:"title"},r.a.createElement("h2",null,"Translate To Morse")),r.a.createElement("div",{id:"input"},r.a.createElement("input",{type:"text",id:"morseInput",value:o,onChange:function(e){e.preventDefault(),i(e.target.value)},placeholder:"Type here.",maxLength:"25"})," ",r.a.createElement("i",{className:"ri-play-fill",onClick:function(){e(d)}}),r.a.createElement("i",{className:"ri-stop-fill",onClick:function(){return e("")}})),r.a.createElement("div",{id:"morseTranslation"},r.a.createElement("span",{id:"morseTrans"},""===d?"Morse translation will appear here.":d.replace(/\?/g,"[?]").replace(/\] /g,"]"))))},fe=function(){var e=Object(a.useState)("nav-learn"),t=Object(l.a)(e,2),n=t[0],o=t[1];function i(e){"nav-learn"===e.target.id?o("nav-learn"):"nav-legend"===e.target.id?o("nav-legend"):o("nav-play"),document.querySelector(".navbar").childNodes.forEach((function(t){t.id===e.target.id?t.classList.add("selected"):t.classList.remove("selected")}))}return r.a.createElement("div",{className:"sidebar",id:"left"},r.a.createElement("div",{id:"sidebar-container"},r.a.createElement("div",{className:"navbar"},r.a.createElement("div",{id:"nav-play",className:"nav-item",onClick:i},"Play"),r.a.createElement("div",{id:"nav-learn",className:"nav-item selected",onClick:i},"Learn"),r.a.createElement("div",{id:"nav-legend",className:"nav-item",onClick:i},"Legend")),r.a.createElement("div",{id:"info-icon",onClick:function(){document.querySelector(".sidebar#left").classList.toggle("hide"),document.querySelector("#main-interface").classList.toggle("expandLeft")}},r.a.createElement("i",{className:"ri-arrow-left-circle-line"})),r.a.createElement("div",{id:"sidebar-content"},"nav-learn"===n&&r.a.createElement(ue,null),"nav-legend"===n&&r.a.createElement("div",{id:"playerAndLegend"},r.a.createElement(he,null),r.a.createElement("span",{id:"note"},"Adjust the Morse code speed by changing the WPM in the Options menu."),r.a.createElement(pe,null)))))},ve=r.a.memo((function(e){var t=Object(a.useContext)(S),n=t.frequency,o=t.setFrequency,i=300,l=1500;return r.a.createElement("div",{id:"frequency",className:"mode-picker"},r.a.createElement("div",{id:"title"},"Frequency ",r.a.createElement("span",{id:"range"},"(",i,"-",l,")")),r.a.createElement("div",{id:"input"},r.a.createElement("button",{id:"freq-down",onClick:function(){o((function(e){return e-10>=i?e-10:i}))}},r.a.createElement("i",{className:"ri-arrow-down-s-line"})),r.a.createElement("input",{type:"number",name:"frequency",id:"frequency-input",value:n,onChange:function(e){Number(e.target.value)>l?o(l):Number(e.target.value)\n {props.children}\n \n )\n}\n\nexport {GameModeContextProvider, GameModeContext}\n","import React, {useState} from \"react\"\n\nconst MorseBufferContext = React.createContext()\n\nfunction MorseBufferContextProvider(props) {\n \n const [morseCharBuffer, setMorseCharBuffer] = useState('')\n const [morseWords, setMorseWords] = useState([])\n \n return (\n \n {props.children}\n \n )\n}\n\nexport {MorseBufferContextProvider, MorseBufferContext}\n","import React, {useState} from \"react\"\nimport alphabet from '../data/alphabet.json'\nimport numbers from '../data/numbers.json'\nimport common100 from '../data/common100.json'\nimport boys from '../data/names_boys.json'\nimport girls from '../data/names_girls.json'\nimport trek from '../data/startrek.json'\n\n\nconst WordListPickerContext = React.createContext()\n\nfunction WordListPickerContextProvider(props) {\n\n const [wordListCategory, setWordListCategory] = useState('alphabet')\n const [wordListCount, setWordListCount] = useState(10)\n\n let wordList = []\n\n if (wordListCategory === 'alphabet') {\n wordList = alphabet.words\n } else if (wordListCategory === 'numbers') {\n wordList = numbers.words\n } else if (wordListCategory === 'boys') {\n wordList = boys.words\n } else if (wordListCategory === 'girls') {\n wordList = girls.words\n } else if (wordListCategory === 'startrek') {\n wordList = trek.words\n } else if (wordListCategory === 'common100') {\n wordList = common100.words\n }\n\n const wordListCountMax = wordList.length\n\n const metadata = {\n 'alphabet': {name: 'Alphabet', description: 'All letters of the alphabet'},\n 'numbers': {name: 'Numbers', description: '0-9'},\n 'boys': {name: 'Boys Names', description: 'Popular Boys Names'},\n 'girls': {name: 'Girls Names', description: 'Popular Girls Names'},\n 'startrek': {name: 'Star Trek', description: 'Star Trek universe'},\n 'common100': {name: 'Common Words', description: '100 Most Common Words'}\n }\n\n // Shuffle input array and return\n function randomize(arr) {\n let array = [...arr]\n let currentIndex = array.length, temporaryValue, randomIndex;\n \n // While there remain elements to shuffle...\n while (0 !== currentIndex) {\n \n // Pick a remaining element...\n randomIndex = Math.floor(Math.random() * currentIndex);\n currentIndex -= 1;\n \n // And swap it with the current element.\n temporaryValue = array[currentIndex];\n array[currentIndex] = array[randomIndex];\n array[randomIndex] = temporaryValue;\n }\n return array;\n }\n\n return (\n \n {props.children}\n \n )\n}\n\nexport {WordListPickerContextProvider, WordListPickerContext}\n","import React, {useState, useContext} from \"react\"\nimport { WordListPickerContext } from \"./wordListPickerContext\"\nconst WordFeederContext = React.createContext()\n\nfunction WordFeederContextProvider(props) {\n\n const {wordList, wordListShuffled} = useContext(WordListPickerContext)\n\n const [wordIndex, setWordIndex] = useState(0)\n const [order, setOrder] = useState('sequential')\n let word \n\n // Set word list ordered appropriately\n if (order === 'sequential') {\n if (wordList[wordIndex] === undefined) {\n word = [wordList[0]]\n }\n else {\n word = wordList[wordIndex]\n }\n }\n else if (order === 'random') {\n if (wordListShuffled[wordIndex] === undefined) {\n word = [wordListShuffled[0]]\n }\n else {\n word = wordListShuffled[wordIndex]\n }\n }\n\n function resetFeeder() {\n setWordIndex(0)\n }\n \n function getNextWord() {\n setWordIndex(prev => prev + 1)\n }\n \n return (\n \n {props.children}\n \n )\n}\n\nexport {WordFeederContextProvider, WordFeederContext}\n","import React, {useState, useContext} from \"react\"\nimport { WordFeederContext } from \"./wordFeederContext\"\nimport { MorseBufferContext } from \"./morseBufferContext\";\nimport morseCode from '../data/morse-reverse.json'\n\n\nconst ChallengeContext = React.createContext()\n\nfunction ChallengeContextProvider(props) {\n\n const [challengeState, setChallengeState] = useState('ready')\n const {resetFeeder} = useContext(WordFeederContext)\n const {word, getNextWord} = useContext(WordFeederContext)\n const {morseCharBuffer, setMorseCharBuffer} = useContext(MorseBufferContext)\n \n \n let morseArray = morseCharBuffer.split('_').filter(l => l !== '')\n let challengeWordClass = ''\n \n let correctCharIndexes = [] // Indexes of correct letters in Challenge Word\n let incorrectMorseIndexes = [] // Indexes of incorrect morse characters in morse character buffer\n \n let offset = 0\n let challengeLetters\n\n\n function startChallenge() {\n\n let countdown\n let count = 3\n\n // Challenge countdown setup\n document.getElementById('challengeReady').classList.add('starting')\n document.getElementById('challengeReady').innerHTML = `Challenge starting in ${count} `\n // Start Challenge countdown\n countdown = setInterval(() => {\n count--\n if (count === 0) {\n // Do this when countdown hits 0\n document.getElementById('challenge-overlay').classList.add('fade')\n clearInterval(countdown)\n setTimeout(() => {\n document.getElementById('challenge-overlay').classList.add('hide')\n // Start Challenge\n setChallengeState('started')\n }, 900);\n }\n document.getElementById('challengeReady').innerHTML = `Challenge starting in ${count} `\n }, 1000)\n }\n\n function completeChallenge() {\n if (challengeState !== 'completed') {\n setChallengeState('completed')\n resetFeeder()\n showOverlay()\n }\n }\n\n function cancelChallenge() {\n if (challengeState !== 'cancelled') {\n setChallengeState('cancelled')\n resetFeeder()\n showOverlay()\n }\n }\n\n function showOverlay() {\n const challengeOverlay = document.getElementById('challenge-overlay')\n challengeOverlay.classList.remove('fade')\n challengeOverlay.classList.remove('hide')\n }\n\n\n // If no more words in wordlist, feeder returns first word in an array\n if (typeof word === 'object') {\n completeChallenge()\n challengeLetters = word[0].split('')\n }\n else {\n challengeLetters = word.split('')\n }\n\n // Iterate through the morse character buffer and compare with each letter of challenge word\n morseArray.forEach((item, index) => { \n if (morseCharBuffer.slice(-1) === '_') { // If end of morse character\n \n let morseLetter = morseCode[morseArray[index]] || '[?]'\n let challengeLetter = challengeLetters[index-offset].toLowerCase()\n \n if (morseLetter === challengeLetter) {\n correctCharIndexes.push(index-offset)\n \n document.getElementById('challengeWord').childNodes[index-offset].classList.add('correct')\n }\n else {\n incorrectMorseIndexes.push(index)\n if (incorrectMorseIndexes.length > 0) {\n setMorseCharBuffer(prev => {\n let newState = prev.split('_').filter(l => l !== '')\n newState.splice(incorrectMorseIndexes[0], 1)\n newState = newState.join('_') + '_'\n \n return newState\n })\n incorrectMorseIndexes.splice(1,incorrectMorseIndexes.length)\n }\n offset = incorrectMorseIndexes.length\n }\n }\n })\n\n\n // Retrieve next word once all characters are correct\n if (correctCharIndexes.length === challengeLetters.length) {\n challengeWordClass = 'correct'\n setTimeout(() => {\n setMorseCharBuffer('')\n morseArray = []\n incorrectMorseIndexes = []\n offset = 0\n if (document.getElementById('challengeWord') !== null) {\n document.getElementById('challengeWord').childNodes.forEach(node => {\n node.classList = \"cLetter\"\n })\n }\n }, 800)\n setTimeout(() => {\n if (correctCharIndexes.length > 0) {\n correctCharIndexes = []\n getNextWord()\n }\n }, 1000)\n }\n\n \n return (\n \n {props.children}\n \n )\n}\n\nexport {ChallengeContextProvider, ChallengeContext}\n","import React, {useState, useContext, useEffect} from \"react\"\nimport { ChallengeContext } from \"./challengeContext\"\nconst GameClockContext = React.createContext()\n\nfunction GameClockContextProvider(props) {\n\n const [gameClockTime, setGameClockTime] = useState(0)\n const [clockIsRunning, setClockIsRunning] = useState(false)\n const [intervals, setIntervals] = useState([])\n const {challengeState, setChallengeState} = useContext(ChallengeContext)\n\n\n function startGameClock() {\n if (!clockIsRunning) {\n setClockIsRunning(true)\n setIntervals(prev => [...prev, (setInterval(() => {\n if (document.getElementById('gameClock') === null) {\n stopGameClock()\n return\n }\n setGameClockTime(prev => prev + 1)\n }, 1000))\n ])\n }\n }\n function stopGameClock() {\n if (clockIsRunning) {\n cleanup()\n setClockIsRunning(false)\n }\n }\n\n // Clear game clock intervals\n function cleanup() {\n for (let i = 0; i < intervals.length; i++) {\n clearInterval(intervals[i]);\n }\n }\n\n // Trigger game clock changes on challenge state change\n useEffect(() => {\n switch (challengeState) {\n case 'ready':\n setGameClockTime(0)\n cleanup()\n break\n case 'started':\n startGameClock()\n break\n case 'completed':\n stopGameClock()\n break\n case 'cancelled':\n stopGameClock()\n setChallengeState('ready')\n break\n default:\n return\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [challengeState])\n \n\n return (\n \n {props.children}\n \n )\n}\n\nexport {GameClockContextProvider, GameClockContext}\n","import React, {useState} from \"react\"\n\nconst WPMContext = React.createContext()\n\nfunction WPMContextProvider(props) {\n \n const [wpm, setWPM] = useState(10)\n\n return (\n \n {props.children}\n \n )\n}\n\nexport {WPMContextProvider, WPMContext}\n","import React, {useState} from \"react\"\n\nconst FrequencyContext = React.createContext()\n\nfunction FrequencyContextProvider(props) {\n \n const [frequency, setFrequency] = useState(650)\n\n return (\n \n {props.children}\n \n )\n}\n\nexport {FrequencyContextProvider, FrequencyContext}\n","import React, {useState} from \"react\"\n\nconst KeyTypeContext = React.createContext()\n\nfunction KeyTypeContextProvider(props) {\n\n const [keyType, setKeyType] = useState('straight')\n\n return (\n \n {props.children}\n \n )\n}\n\nexport {KeyTypeContextProvider, KeyTypeContext}\n","import {useEffect, useContext} from 'react'\nimport { FrequencyContext } from '../contexts/frequencyContext'\nimport { GameModeContext } from '../contexts/gameModeContext'\nimport { MorseBufferContext } from '../contexts/morseBufferContext'\nimport { WPMContext } from '../contexts/wpmContext'\nimport config from '../config.json'\n\n// ELECTRONIC KEY TELEGRAPH - Iambic A\n\nexport default (function useElectronicKey() {\n\n const {morseCharBuffer, setMorseCharBuffer, morseWords, setMorseWords} = useContext(MorseBufferContext)\n const {wpm} = useContext(WPMContext)\n const {gameMode} = useContext(GameModeContext)\n const {frequency} = useContext(FrequencyContext)\n\n // DitDah length setup\n let ditMaxTime = 1200/wpm\n let ratio = .2\n const letterGapMinTime = ditMaxTime*ratio*3\n const wordGapMaxTime = ditMaxTime*ratio*7\n\n const morseHistorySize = config.historySize\n \n let leftIsPressed = false\n let rightIsPressed = false\n let queueRunning = false\n let queue = []\n let pressedFirst = null\n\n // Timers setup\n let depressSyncTime\n let depressSyncTimer\n let depressSyncTimerRunning = false\n let gapTime = 0\n let gapTimer = 0\n\n let paddlesReleasedSimultaneously = false\n\n let currentPromise = Promise.resolve()\n\n // Audio Setup\n let AudioContext = window.AudioContext || window.webkitAudioContext || false\n let context\n window.AudioContext = window.AudioContext || window.webkitAudioContext;\n if (AudioContext) {\n context = new AudioContext()\n } else {\n context = null\n }\n\n // Promisify playing Dits and Dahs\n function play(ditDah) {\n let playDuration = ((ditDah === '.') ? ditMaxTime : ditMaxTime*3)\n\n return new Promise((resolve, reject) => {\n if (context.state === 'interrupted') {\n context.resume()\n }\n \n let o = context.createOscillator()\n o.frequency.value = frequency\n o.type = \"sine\"\n o.onended = () => {\n resolve()\n }\n \n let startTime = context.currentTime;\n \n let g = context.createGain()\n g.gain.exponentialRampToValueAtTime(config.mainVolume, startTime)\n g.gain.setValueAtTime(config.mainVolume, startTime)\n o.connect(g)\n g.connect(context.destination)\n \n o.start(startTime)\n \n setTimeout(() => {\n g.gain.setTargetAtTime(0.0001, context.currentTime, 0.001)\n o.stop(context.currentTime + 0.05)\n }, playDuration)\n })\n }\n\n // Play dit or dah with trailing space (silence)\n function playWithSpaces(ditDah) {\n let delay = (ditDah === '.') ? ditMaxTime + ditMaxTime : ditMaxTime*3 + ditMaxTime\n\n return new Promise(function(resolve) {\n if (ditDah === '.' || ditDah === '-') {\n\n clearInterval(gapTimer)\n checkGapBetweenInputs()\n setMorseCharBuffer(prev => prev + ditDah)\n \n play(ditDah)\n .then(setTimeout(() => {\n // START GAP TIMER\n gapTimer = setInterval(() => {\n gapTime += 1\n \n // if (gapTime >= wordGapMaxTime) {\n if (gameMode === 'practice' && gapTime >= wordGapMaxTime) {\n setMorseCharBuffer(prev => prev + '/')\n clearInterval(gapTimer)\n gapTimer = 0\n gapTime = 0\n }\n else if (gameMode === 'challenge' && gapTime >= letterGapMinTime) {\n setMorseCharBuffer(prev => prev + '_')\n clearInterval(gapTimer)\n gapTimer = 0\n gapTime = 0\n }\n }, 1)\n \n resolve();\n }, delay)\n )\n } else {\n setTimeout(() => {\n resolve();\n }, ditMaxTime*3)\n }\n });\n }\n\n function executeQueue() {\n let localQueue = queue\n \n // Set waitTime to completion of queue (ditDah time + following silences)\n let waitTime = 0\n for (let i in localQueue) {\n if (localQueue[i] === '.') {\n waitTime += ditMaxTime*2\n } else if (localQueue[i] === '-') {\n waitTime += ditMaxTime*4\n }\n }\n \n // Cleanup\n function cleanup() {\n queueRunning = false\n queue = []\n sendPressedToQueue() // Check if anything still pressed down on queue finish\n }\n \n // Wait till completion of queue to execute\n const clear = setTimeout(() => {\n cleanup()\n }, waitTime)\n \n // Execute queue\n queueRunning = true\n for (let i = 0; i < localQueue.length; i++) {\n if (paddlesReleasedSimultaneously) {\n localQueue.pop()\n clearTimeout(clear)\n cleanup()\n }\n currentPromise = currentPromise.then(() => {\n return playWithSpaces(localQueue[i])\n });\n }\n }\n\n // Determine which paddles are pressed, add to queue, and execute\n function sendPressedToQueue() {\n if (leftIsPressed && rightIsPressed) {\n if (pressedFirst === 'left') {\n queue.push('-')\n pressedFirst = null\n } else {\n queue.push('.')\n queue.push('-')\n }\n }\n else if (leftIsPressed && !rightIsPressed) {\n queue.push('.')\n }\n else if (rightIsPressed && !leftIsPressed) {\n queue.push('-')\n }\n if (queue.length > 0) {\n executeQueue()\n }\n }\n\n\n function handleInputStart(event) {\n if (event.type === 'touchstart') {\n event.preventDefault()\n }\n\n paddlesReleasedSimultaneously = false\n\n if (event.keyCode === 188 || event.keyCode === 190) {\n if (document.activeElement.id === 'morseInput') {\n return\n } else if (document.activeElement.tagName.toLowerCase() !== 'body') {\n event.preventDefault()\n document.activeElement.blur()\n }\n }\n if (event.repeat) { return }\n\n if (event.keyCode === 188 || event.target.id === \"left\") {\n document.querySelector('.paddle#left').classList.add('active')\n \n leftIsPressed = true\n if (!rightIsPressed) { pressedFirst = 'left'}\n\n // Prevent further input if queue is executing\n if (!queueRunning) {\n sendPressedToQueue()\n }\n }\n else if (event.keyCode === 190 || event.target.id === \"right\") {\n document.querySelector('.paddle#right').classList.add('active')\n\n rightIsPressed = true\n if (!leftIsPressed) { pressedFirst = 'right'}\n\n // Prevent further input if queue is executing\n if (!queueRunning) {\n sendPressedToQueue()\n }\n }\n }\n\n function handleInputEnd(event) {\n if (event.type === 'touchend') {\n event.preventDefault()\n }\n\n if (event.keyCode === 188 || event.target.id === \"left\") {\n document.querySelector('.paddle#left').classList.remove('active')\n\n leftIsPressed = false\n \n if (pressedFirst === 'left') { pressedFirst = null }\n\n if (!depressSyncTimerRunning) { startDepressSyncTimer() }\n else { stopDepressSyncTimer() }\n }\n if (event.keyCode === 190 || event.target.id === \"right\") {\n document.querySelector('.paddle#right').classList.remove('active')\n\n rightIsPressed = false\n if (pressedFirst === 'right') { pressedFirst = null }\n\n if (!depressSyncTimerRunning) { startDepressSyncTimer() }\n else { stopDepressSyncTimer() }\n }\n }\n \n // Timer used to determine if both paddles are released within 10ms\n // Need to know this to stop Iambic tones at correct time\n function startDepressSyncTimer() {\n depressSyncTimerRunning = true\n // Reset depressSyncTime\n depressSyncTime = 0\n // Start depressSyncTimer\n depressSyncTimer = setInterval(() => {\n depressSyncTime += 1\n if (depressSyncTime > 20) {\n depressSyncTimerRunning = false\n clearInterval(depressSyncTimer)\n depressSyncTime = 0\n }\n }, 1);\n }\n function stopDepressSyncTimer() {\n depressSyncTimerRunning = false\n clearInterval(depressSyncTimer)\n if (depressSyncTime < 10) {\n paddlesReleasedSimultaneously = true\n queue.pop()\n }\n depressSyncTime = 0\n }\n\n\n // Check gap between letters to determin if new character or new word\n function checkGapBetweenInputs() {\n if (gapTime >= letterGapMinTime && gapTime < wordGapMaxTime) {\n if (gameMode === 'practice') {\n setMorseCharBuffer(prev => prev + ' ')\n } else if (gameMode === 'challenge') {\n setMorseCharBuffer(prev => prev + '_')\n }\n gapTime = 0\n clearInterval(gapTimer)\n gapTimer = 0\n }\n }\n\n // Add paddle event listeners and update on WPM, Game Mode, or Frequency change\n // Not updating on these state changes prevents change from taking effect\n useEffect(() => {\n document.addEventListener('keydown', handleInputStart)\n document.addEventListener('keyup', handleInputEnd)\n\n const paddles = document.querySelectorAll('.paddle')\n paddles.forEach(paddle => {\n paddle.addEventListener('mousedown', handleInputStart)\n paddle.addEventListener('touchstart', handleInputStart)\n paddle.addEventListener('mouseout', handleInputEnd)\n paddle.addEventListener('mouseup', handleInputEnd)\n paddle.addEventListener('touchend', handleInputEnd)\n })\n\n return function cleanup() {\n document.removeEventListener('keydown', handleInputStart)\n document.removeEventListener('keyup', handleInputEnd)\n\n const paddles = document.querySelectorAll('.paddle')\n paddles.forEach(paddle => {\n paddle.removeEventListener('mousedown', handleInputStart)\n paddle.removeEventListener('touchstart', handleInputStart)\n paddle.removeEventListener('mouseout', handleInputEnd)\n paddle.removeEventListener('mouseup', handleInputEnd)\n paddle.removeEventListener('touchend', handleInputEnd)\n })\n\n clearInterval(depressSyncTimer)\n clearInterval(gapTimer)\n }\n // eslint-disable-next-line\n }, [wpm, gameMode, frequency])\n\n // Remove forward slash and move buffer contents to morse words array\n useEffect(() => {\n if (morseCharBuffer.slice(-1) === '/' && gameMode === 'practice') {\n let val = morseCharBuffer.slice(0,morseCharBuffer.length-1)\n setMorseWords(prev => [val, ...prev])\n\n // Limit history to configured history size\n if (morseWords.length >= morseHistorySize) {\n setMorseWords(prev => prev.slice(0,prev.length-1))\n }\n setMorseCharBuffer('')\n }\n\n // eslint-disable-next-line\n }, [morseCharBuffer])\n\n})","import React from \"react\"\nimport useElectronicKey from '../hooks/useElectronicKey';\n\nexport default React.memo(function ElectronicKey() {\n useElectronicKey()\n})","import React from \"react\"\n\nexport default (function DitDahDisplay(props) {\n\n return (props.dd === ' ') ?
: {props.dd}
\n})","import React, { useContext } from \"react\"\nimport { MorseBufferContext } from \"../contexts/morseBufferContext\"\nimport DitDahDisplay from \"./DitDahDisplay\"\nimport morseCode from '../data/morse-reverse.json'\n\nexport default React.memo(function MorseBufferDisplay() {\n \n const {morseCharBuffer} = useContext(MorseBufferContext)\n\n let ditDahs = morseCharBuffer.split('').map((ditdah,index) => )\n let alphanumeric = ''\n let letters = morseCharBuffer.split(' ')\n\n if (morseCharBuffer === '') {}\n else {\n for (let i in letters) {\n if (letters[i] === ' ') {\n alphanumeric += ' '\n } else {\n if (morseCode[letters[i]] === undefined) {\n alphanumeric += (letters[i] === '' ? '':'[?]')\n } else {\n alphanumeric += morseCode[letters[i]]\n }\n }\n }\n }\n\n return (\n \n
\n
\n
\n
\n {alphanumeric.toUpperCase()}\n
\n
\n
\n )\n})","import React, {useContext} from \"react\"\nimport { MorseBufferContext } from \"../contexts/morseBufferContext\"\nimport morseCode from '../data/morse-reverse.json'\n\nexport default (function MorseHistoryTextBox() {\n\n const {morseWords, setMorseWords} = useContext(MorseBufferContext)\n\n let text = ''\n\n function clearHistory() {\n setMorseWords([])\n }\n\n // Generate Morse History contents\n morseWords.forEach((word, index) => {\n if (word.includes(' ')) {\n let newWord = ''\n word.split(' ').forEach(letter => {\n if (morseCode[letter] === undefined) {\n newWord += '[?]'\n } else {\n newWord += morseCode[letter].toUpperCase()\n }\n })\n text = newWord + ' ' + text\n }\n else if (morseCode[word] === undefined) {\n text = '[?] ' + text\n } else {\n text = morseCode[word].toUpperCase() + ' ' + text\n }\n })\n\n return (\n \n
{text}
\n
\n \"[?] \" signifies no translation available. \n Clear \n
\n
\n )\n})","import { useEffect, useContext } from 'react'\nimport { FrequencyContext } from '../contexts/frequencyContext'\nimport { GameModeContext } from '../contexts/gameModeContext'\nimport { MorseBufferContext } from '../contexts/morseBufferContext'\nimport { WPMContext } from '../contexts/wpmContext'\nimport config from '../config.json'\n\n// STRAIGHT KEY TELEGRAPH\nexport default (function useStraightKey() {\n \n const {morseCharBuffer, setMorseCharBuffer, morseWords, setMorseWords} = useContext(MorseBufferContext)\n const {wpm} = useContext(WPMContext)\n const {gameMode} = useContext(GameModeContext)\n const {frequency} = useContext(FrequencyContext)\n\n // Spacing time and timer setup\n let charTimer = 0\n let charTime = 0\n let gapTimer = 0\n let gapTime = 0\n \n // DitDah Length\n const ditMaxTime = 1200/wpm * 0.3\n const letterGapMinTime = ditMaxTime*3\n const wordGapMaxTime = ditMaxTime*7\n\n const morseHistorySize = config.historySize\n\n // Tone Setup\n let AudioContext = window.AudioContext || window.webkitAudioContext || false\n let context\n window.AudioContext = window.AudioContext || window.webkitAudioContext;\n if (AudioContext) {\n context = new AudioContext()\n } else {\n context = null\n }\n\n let o // Oscillator Node\n let g // Gain Node\n\n let isRunning = false\n\n function handleInputStart(event) {\n if (event.type === 'touchstart') {\n event.preventDefault()\n }\n\n if (event.keyCode === 32) {\n if (document.activeElement.id === 'morseInput') {\n return\n }\n else if (document.activeElement.tagName.toLowerCase() !== 'body') {\n event.preventDefault()\n document.activeElement.blur()\n }\n }\n\n if (isRunning) {\n return\n } else {\n isRunning = true\n\n if ((event.keyCode !== 32 &&\n event.target.id !== \"morseButton\" &&\n event.target.className !== \"paddle\") ||\n (event.repeat)) {\n return\n }\n else {\n document.getElementById('morseButton').classList.add('active')\n \n // isRunning = true\n \n if (context.state === 'interrupted') {\n context.resume()\n }\n \n o = context.createOscillator()\n o.frequency.value = frequency\n o.type = \"sine\"\n \n g = context.createGain()\n g.gain.exponentialRampToValueAtTime(config.mainVolume, context.currentTime)\n o.connect(g)\n g.connect(context.destination)\n o.start()\n \n checkGapBetweenInputs()\n clearInterval(gapTimer)\n \n startCharTimer()\n }\n }\n \n }\n \n function startCharTimer() {\n // Start Character Timer\n charTimer = setInterval(() => {\n charTime += 1\n }, 1);\n }\n\n function handleInputEnd(event) {\n if (isRunning) {\n if ((event.keyCode !== 32 &&\n event.target.id !== \"morseButton\" &&\n event.target.className !== \"paddle\") ||\n (event.repeat)) {\n return\n }\n\n document.getElementById('morseButton').classList.remove('active')\n\n isRunning = false\n \n if (charTime <= ditMaxTime) {\n setMorseCharBuffer(prev => prev + '.')\n } else {\n setMorseCharBuffer(prev => prev + '-')\n }\n \n stopCharTimer()\n startGapTimer()\n \n // Account for bug triggered when pressing paddle button (e.g.) outside of body, then clicking into body, and depressing key\n if (o === undefined) { \n return\n }\n if (o.context.state === 'running') {\n g.gain.setTargetAtTime(0.0001, context.currentTime, 0.001)\n o.stop(context.currentTime + 0.05)\n }\n } else { return }\n }\n\n function stopCharTimer() { \n clearInterval(charTimer)\n charTimer = 0\n charTime = 0\n }\n\n function startGapTimer() {\n gapTime = 0\n gapTimer = setInterval(() => {\n gapTime += 1\n\n // Gap between words\n if (gameMode === 'practice' && gapTime >= wordGapMaxTime) {\n setMorseCharBuffer(prev => prev + '/')\n clearInterval(gapTimer)\n gapTimer = 0\n gapTime = 0\n }\n else if (gameMode === 'challenge' && gapTime >= letterGapMinTime) {\n setMorseCharBuffer(prev => prev + '_')\n clearInterval(gapTimer)\n gapTimer = 0\n gapTime = 0\n }\n }, 1);\n }\n\n // Check gap between letters to determin if new character or new word\n function checkGapBetweenInputs() {\n if (gapTime >= letterGapMinTime && gapTime < wordGapMaxTime) {\n if (gameMode === 'practice') {\n setMorseCharBuffer(prev => prev + ' ')\n } else if (gameMode === 'challenge') {\n setMorseCharBuffer(prev => prev + '_')\n }\n clearInterval(gapTimer)\n gapTimer = 0\n }\n }\n \n // Add paddle event listeners and update on WPM, Game Mode, or Frequency change\n // Not updating on these state changes prevents change from taking effect\n useEffect(() => {\n document.addEventListener('keydown', handleInputStart)\n document.addEventListener('keyup', handleInputEnd)\n\n const morseButton = document.getElementById('morseButton')\n morseButton.addEventListener('touchstart', handleInputStart)\n morseButton.addEventListener('touchend', handleInputEnd)\n morseButton.addEventListener('mousedown', handleInputStart)\n morseButton.addEventListener('mouseout', handleInputEnd)\n morseButton.addEventListener('mouseup', handleInputEnd)\n\n return function cleanup() {\n document.removeEventListener('keydown', handleInputStart)\n document.removeEventListener('keyup', handleInputEnd)\n\n const morseButton = document.getElementById('morseButton')\n morseButton.removeEventListener('touchstart', handleInputStart)\n morseButton.removeEventListener('touchend', handleInputEnd)\n morseButton.removeEventListener('mousedown', handleInputStart)\n morseButton.removeEventListener('mouseout', handleInputEnd)\n morseButton.removeEventListener('mouseup', handleInputEnd)\n\n clearInterval(charTimer)\n clearInterval(gapTimer)\n }\n // eslint-disable-next-line\n }, [wpm, gameMode, frequency])\n\n // Remove forward slash and move buffer contents to morse words array\n useEffect(() => {\n if (morseCharBuffer.slice(-1) === '/' && gameMode === 'practice') {\n let val = morseCharBuffer.slice(0,morseCharBuffer.length-1)\n setMorseWords(prev => [val, ...prev])\n\n // Limit history to configured history size\n if (morseWords.length >= morseHistorySize) {\n setMorseWords(prev => prev.slice(0,prev.length-1))\n }\n setMorseCharBuffer('')\n\n // Scroll morse history textbox to bottom (scrolling enabled on mobile screens)\n let morseHistory = document.getElementById(\"morseHistory-textbox\");\n morseHistory.scrollTop = morseHistory.scrollHeight;\n }\n\n // eslint-disable-next-line\n }, [morseCharBuffer])\n\n})","import React from \"react\"\nimport useStraightKey from '../hooks/useStraightKey';\n\nexport default React.memo(function StraightKey() {\n useStraightKey()\n})","import React, { useContext } from 'react';\nimport '../css/App.css';\nimport { KeyTypeContext } from '../contexts/keyTypeContext';\nimport ElectronicKey from '../components/ElectronicKey';\nimport MorseBufferDisplay from '../components/MorseBufferDisplay'\nimport MorseHistoryTextBox from '../components/MorseHistory'\nimport StraightKey from '../components/StraightKey';\n\n\nexport default (function PracticeMode() {\n\n const {keyType} = useContext(KeyTypeContext)\n\n return (\n <>\n {keyType === \"straight\" ?\n : \n }\n \n \n >\n );\n\n \n})","import React from \"react\"\n\nexport default React.memo(function ChallengeBufferDisplay(props) {\n\n const morseArray = props.morseArray\n\n let ditDahs = []\n\n for (let i in morseArray) {\n let morseChar = morseArray[i]\n \n ditDahs.push({morseChar} )\n }\n\n return (\n \n )\n})","import React from \"react\"\n\nexport default (function ChallengeControls(props) {\n\n return (\n \n Exit Challenge \n
\n )\n})","import React, { useContext } from \"react\"\nimport { WordFeederContext } from \"../contexts/wordFeederContext\"\n\nexport default React.memo(function ChallengeWord(props) {\n\n const {word} = useContext(WordFeederContext)\n\n let challengeLetters\n if (typeof word === 'object') {\n challengeLetters = word[0].split('')\n }\n else {\n challengeLetters = word.split('')\n }\n\n let spannedWord = challengeLetters.map((letter,index) => {letter} )\n\n return (\n {spannedWord}
\n )\n})","import React, {useContext} from \"react\"\nimport { GameClockContext } from \"../contexts/gameClockContext\";\n\nexport default (function GameClock(props) {\n \n const {gameClockTime} = useContext(GameClockContext)\n\n const minutes = Math.floor(gameClockTime / 60)\n const seconds = gameClockTime % 60\n\n return (\n Time Elapsed: {minutes} minutes {seconds} seconds
\n )\n})","import React, {useContext} from 'react';\nimport '../css/App.css';\nimport { ChallengeContext } from '../contexts/challengeContext';\nimport { KeyTypeContext } from '../contexts/keyTypeContext';\nimport ChallengeBufferDisplay from '../components/ChallengeBufferDisplay';\nimport ChallengeControls from '../components/ChallengeControls';\nimport ChallengeWord from '../components/ChallengeWord'\nimport ElectronicKey from '../components/ElectronicKey';\nimport GameClock from '../components/GameClock';\nimport StraightKey from '../components/StraightKey';\n\n\nexport default React.memo(function ChallengeMode() {\n \n const {keyType} = useContext(KeyTypeContext)\n const {challengeState, cancelChallenge, morseArray, challengeWordClass} = useContext(ChallengeContext)\n\n return (\n <>\n {challengeState === 'started' ? (keyType === \"straight\" ?\n : ) : <>>\n }\n \n \n \n >\n )\n});\n","import React, {useContext} from \"react\"\nimport { ChallengeContext } from \"../contexts/challengeContext\"\nimport { GameClockContext } from \"../contexts/gameClockContext\"\nimport { GameModeContext } from \"../contexts/gameModeContext\"\nimport { MorseBufferContext } from \"../contexts/morseBufferContext\"\nimport { WordFeederContext } from \"../contexts/wordFeederContext\"\n\nexport default React.memo(function ModePicker() {\n\n const {setGameMode} = useContext(GameModeContext)\n const {setMorseCharBuffer} = useContext(MorseBufferContext)\n const {resetFeeder} = useContext(WordFeederContext)\n const {stopGameClock, setGameClockTime, clockIsRunning} = useContext(GameClockContext)\n const {setChallengeState} = useContext(ChallengeContext)\n\n function handleClick(e) {\n setMorseCharBuffer('')\n resetFeeder()\n setChallengeState('ready')\n\n setGameMode(e.target.id)\n\n if (clockIsRunning) { \n stopGameClock()\n setGameClockTime(0)\n }\n\n let buttons = document.querySelector(\".mode-picker#gameMode #buttons\").childNodes\n buttons.forEach(button => {\n if (button.id === e.target.id) {\n button.classList.add('selected')\n } else { button.classList.remove('selected')}\n })\n }\n\n return (\n \n
\n Mode\n
\n
\n \n Practice Mode\n \n \n Challenge Mode\n \n
\n
\n )\n})","import React, {useContext, useEffect} from \"react\"\nimport {KeyTypeContext} from \"../contexts/keyTypeContext\"\n\n\nexport default React.memo(function KeyTypePicker() {\n\n const {setKeyType, keyType} = useContext(KeyTypeContext)\n\n function handleClick(e) {\n setKeyType(e.target.id)\n\n let buttons = document.querySelector(\".mode-picker#keyType #buttons\").childNodes\n buttons.forEach(button => {\n if (button.id === e.target.id) {\n button.classList.add('selected')\n } else { button.classList.remove('selected')}\n })\n\n if (e.target.id === 'electronic') {\n document.querySelector('#morseButton').classList.add('showPaddles')\n document.querySelector('.paddle').classList.add('showPaddles')\n document.querySelector('.paddle#left').classList.add('showPaddles')\n document.querySelector('.paddle#right').classList.add('showPaddles')\n document.getElementById('morseButtonText').innerHTML = 'TAP/HOLD BUTTONS OR PRESS COMMA / PERIOD'\n } else {\n document.querySelector('#morseButton').classList.remove('showPaddles')\n document.querySelector('.paddle').classList.remove('showPaddles')\n document.querySelector('.paddle#left').classList.remove('showPaddles')\n document.querySelector('.paddle#right').classList.remove('showPaddles')\n document.getElementById('morseButtonText').innerHTML = 'TAP BUTTON OR PRESS SPACEBAR'\n }\n }\n\n useEffect(() => {\n document.querySelector(`button#${keyType}`).classList.add('selected')\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n return (\n \n
\n Key Type\n
\n
\n \n Straight Key\n \n \n Electronic Key\n \n
\n
\n )\n})","const shareLinks = {\n 'twitter': {\n name: 'Twitter',\n icon: 'ri-twitter-fill',\n link:'https://twitter.com/intent/tweet?text=Check%20out%20this%20site%20that%20helps%20you%20learn%20Morse%20Code%3A%20https%3A//learnmorsecode.net%20%40genemecija%20%23morse%20%23morsecode'\n },\n 'facebook': {\n name: 'Facebook',\n icon: 'ri-facebook-box-fill',\n link: 'https://www.facebook.com/sharer/sharer.php?u=https%3A//learnmorsecode.net'\n },\n 'email': {\n name: 'Email',\n icon: \"ri-mail-line\",\n link: 'mailto:?subject='+encodeURIComponent('Check out this site that helps you learn Morse code! https://learnmorsecode.net')\n }\n}\nconst contactLinks = {\n 'email': {\n name: 'Email',\n icon: \"ri-mail-line\",\n link: 'mailto:gene@genemecija.com?subject='+encodeURIComponent('Hello, Gene!')\n },\n 'github': {\n name: 'GitHub',\n icon: 'ri-github-fill',\n link: 'https://github.com/genemecija/learn-morse-code/'\n },\n 'twitter': {\n name: 'Twitter',\n icon: 'ri-twitter-fill',\n link:'https://twitter.com/genemecija'\n }\n}\n\nexport {shareLinks, contactLinks}","import React from \"react\"\nimport { shareLinks } from \"../data/social\"\n\nexport default (function Header () {\n\n function PopupCenter(url, title, w, h) { \n // Credit: http://www.xtf.dk/2011/08/center-new-popup-window-even-on.html\n \n // Fixes dual-screen position Most browsers Firefox \n const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : window.screen.left; \n const dualScreenTop = window.screenTop !== undefined ? window.screenTop : window.screen.top; \n \n const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : window.screen.width; \n let height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : window.screen.height; \n \n const left = ((width / 2) - (w / 2)) + dualScreenLeft; \n const top = ((height / 2) - (h / 2)) + dualScreenTop; \n const newWindow = window.open(url, title, 'scrollbars=yes, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left); \n \n // Puts focus on the newWindow \n if (window.focus) { \n newWindow.focus(); \n } \n }\n \n function handleClick(event) {\n let link = event.target.id\n let url = shareLinks[link]['link']\n let title = 'Share'\n let width = '900'\n let height = '500'\n if (link === 'email') {\n width = '150'\n height = '150'\n }\n PopupCenter(url, title, width, height)\n }\n\n let contacts = Object.keys(shareLinks).map((contact, index) => {\n return (\n \n )\n })\n\n return (\n \n )\n})","import { useContext } from 'react';\nimport config from '../config.json'\nimport { WPMContext } from '../contexts/wpmContext.js';\nimport { FrequencyContext } from '../contexts/frequencyContext.js';\n\nexport default (function useMorsePlayer() {\n\n const {wpm} = useContext(WPMContext)\n const {frequency} = useContext(FrequencyContext)\n // const ditMaxTime = 85 //config.ditMaxTime\n const ditMaxTime = 1200/wpm\n\n // Tone Setup\n let AudioContext = window.AudioContext || window.webkitAudioContext\n window.AudioContext = window.AudioContext || window.webkitAudioContext;\n let context\n if (AudioContext) {\n context = new AudioContext()\n } else {\n context = null\n }\n\n // Play dit or dah\n function play(ditDah) {\n let length = ((ditDah === '.') ? ditMaxTime : ditMaxTime*3)\n \n if (context.state === 'interrupted') {\n context.resume()\n }\n let o\n o = context.createOscillator()\n o.frequency.value = frequency\n o.type = \"sine\"\n \n let startTime = context.currentTime;\n\n let g = context.createGain()\n g.gain.exponentialRampToValueAtTime(config.mainVolume, startTime)\n g.gain.setValueAtTime(config.mainVolume, startTime)\n o.connect(g)\n g.connect(context.destination)\n o.start(startTime)\n \n setTimeout(() => {\n g.gain.setTargetAtTime(0.0001, context.currentTime, 0.009)\n o.stop(context.currentTime + 0.05)\n }, length)\n }\n\n let queue = []\n let timeouts = []\n \n function playMorseWord(morse) {\n // Empty morse queue and cancel all sounds (timeouts)\n queue = []\n for (let i = 0; i < timeouts.length; i++) {\n clearTimeout(timeouts[i]);\n }\n\n queue = Array.from(morse)\n let delay = 0\n let firstWord = true\n\n // Iterate through queue, playing dits/dahs sequentially after appropriate delays\n for (let i = 0; i < queue.length; i++) {\n let char = queue[i]\n if (char === '.') {\n if (firstWord) {\n firstWord = false\n timeouts.push(setTimeout(() => {\n play(char)\n }, 0))\n } else {\n timeouts.push(setTimeout(() => {\n play(char)\n }, delay))\n }\n delay += ditMaxTime*2\n } else if (char === '-') {\n if (firstWord) {\n firstWord = false\n timeouts.push(setTimeout(() => {\n play(char)\n }, 0))\n } else {\n timeouts.push(setTimeout(() => {\n play(char)\n }, delay))\n }\n delay += ditMaxTime*4\n } else if (char === ' ') {\n timeouts.push(setTimeout(() => {\n \n }, delay))\n delay += ditMaxTime*2\n } else if (char === '/') {\n timeouts.push(setTimeout(() => {\n \n }, delay))\n delay += ditMaxTime*6\n }\n }\n }\n\n return { playMorseWord, play }\n})","import React, {useContext} from \"react\"\nimport { WPMContext } from \"../contexts/wpmContext\";\nimport useMorsePlayer from \"../hooks/useMorsePlayer\";\n\nexport default React.memo(function WordsPerMinute() {\n\n const {wpm, setWPM} = useContext(WPMContext)\n const {playMorseWord} = useMorsePlayer()\n\n const minWPM = 5\n const maxWPM = 30\n\n function handleChange(e) {\n if (Number(e.target.value) > maxWPM) {\n setWPM(maxWPM)\n } else if (Number(e.target.value) < minWPM) {\n setWPM(minWPM)\n } else {\n setWPM(Number(e.target.value))\n }\n }\n\n function increment() {\n setWPM(prevWPM => {\n if (prevWPM + 1 <= maxWPM) {\n return (prevWPM + 1)\n } else {\n return maxWPM\n }\n })\n }\n function decrement() {\n setWPM(prevWPM => {\n if (prevWPM - 1 >= minWPM) {\n return (prevWPM - 1)\n } else {\n return minWPM\n }\n })\n }\n \n return (\n \n
\n WPM ({minWPM}-{maxWPM}) \n
\n
\n \n \n \n Test playMorseWord('.....')}> \n
\n
\n )\n})","import React from \"react\"\n\nexport default React.memo(function MorseButtons() {\n\n return (\n <>\n \n \n \n
\n \n TAP BUTTON OR PRESS SPACEBAR\n
\n >\n )\n})","import React from \"react\"\nimport { contactLinks } from \"../data/social\"\n\nexport default (function Footer() {\n\n function handleClick(event) {\n window.open(contactLinks[event.target.id]['link'])\n }\n \n return (\n \n )\n})","import React, {useContext} from \"react\"\nimport { WordListPickerContext } from \"../contexts/wordListPickerContext\";\nimport { WordFeederContext } from \"../contexts/wordFeederContext\";\n\nexport default React.memo(function WordCountPicker() {\n\n const {setWordListCount, wordListCountMax} = useContext(WordListPickerContext)\n const {resetFeeder} = useContext(WordFeederContext)\n\n function handleChange(e) {\n resetFeeder()\n setWordListCount(e.target.value)\n }\n\n // Create Options for Select element\n let options = []\n for (let i = 0; i < wordListCountMax; i++) {\n options.push({i+1} )\n }\n \n return (\n \n
\n Challenge Word Count: (1-{wordListCountMax}) \n
\n
\n \n {options}\n \n
\n
\n )\n})","import React, {useContext} from \"react\"\nimport { WordListPickerContext } from \"../contexts/wordListPickerContext\";\nimport { WordFeederContext } from \"../contexts/wordFeederContext\";\nimport WordCountPicker from \"./WordCountPicker\";\n\nexport default React.memo(function WordListPicker() {\n\n const {wordListCategory, setWordListCategory, metadata} = useContext(WordListPickerContext)\n const {resetFeeder, setOrder} = useContext(WordFeederContext)\n\n const orderOpts = ['sequential', 'random']\n\n function handleClick(e) {\n resetFeeder()\n\n // Handle Word List Order selection\n if (orderOpts.includes(e.target.id)) {\n let buttons = document.querySelector(\".mode-picker#wordOrderPicker #buttons\").childNodes\n buttons.forEach(button => {\n if (button.id === e.target.id) {\n button.classList.add('selected')\n } else { button.classList.remove('selected')}\n })\n setOrder(e.target.id)\n }\n // Handle Word List Category selection\n else {\n setWordListCategory(e.target.value)\n }\n }\n\n let wordLists = Object.keys(metadata)\n // Create Option elements for Select element\n let options = wordLists.map((wl, index) => ({metadata[wl]['name']} ))\n\n return (\n \n
\n
\n Word List:\n
\n
\n \n {options}\n \n
\n
\n\n
\n
\n Word Order:\n
\n
\n \n Sequential\n \n \n Random\n \n
\n
\n\n
\n\n
\n
\n Description:\n
\n
\n {metadata[wordListCategory]['description']}\n
\n
\n
\n )\n})","import React, { useContext } from \"react\"\nimport { ChallengeContext } from \"../contexts/challengeContext\"\nimport WordListPicker from \"./WordListPicker\"\n\nexport default (function ChallengeReady() {\n\n const {startChallenge} = useContext(ChallengeContext)\n\n return (\n \n Challenge Options \n \n Start Challenge \n
\n )\n})\n\n","import React, { useContext } from \"react\"\nimport { GameClockContext } from \"../contexts/gameClockContext\"\nimport { ChallengeContext } from \"../contexts/challengeContext\"\nimport { WordListPickerContext } from \"../contexts/wordListPickerContext\"\n\nexport default (function ChallengeComplete() {\n\n const {gameClockTime} = useContext(GameClockContext)\n const {setChallengeState} = useContext(ChallengeContext)\n const {wordListCount, wordListCategory, metadata} = useContext(WordListPickerContext)\n\n function _continue() {\n setChallengeState('ready')\n }\n\n const minutes = Math.floor(gameClockTime / 60)\n const seconds = gameClockTime % 60\n\n let time = (minutes === 0) ? `${seconds} seconds` : `${minutes} minutes and ${seconds} seconds`\n\n return (\n \n Challenge Complete \n You completed {wordListCount} words \n from the {metadata[wordListCategory]['name']} word list \n in {time} ! \n Continue \n\n
\n )\n})\n\n","import React, { useContext } from \"react\"\nimport { ChallengeContext } from \"../contexts/challengeContext\"\nimport ChallengeReady from \"./ChallengeReady\"\nimport ChallengeComplete from \"./ChallengeComplete\"\n\nexport default (function ChallengeOverlay() {\n\n const {challengeState, setChallengeState} = useContext(ChallengeContext)\n \n return (\n \n {challengeState === 'ready' && }\n {challengeState === 'completed' && }\n
\n )\n})","import React from \"react\"\nimport useMorsePlayer from \"../hooks/useMorsePlayer\"\nimport straight_key from \"../media/images/straight_key.jpg\"\nimport electronic_key from \"../media/images/electronic_key.jpg\"\n\nexport default React.memo(function Info() {\n\n const {playMorseWord} = useMorsePlayer()\n\n return (\n \n
Morse Code \n
Morse code is a method of communication that uses short tones (dits) and long tones (dahs) in various sequences to make letters, numbers, and special characters. This tool will help beginners learn Morse code.
\n\n
Dits and Dahs \n
\n Dit playMorseWord('.')}> (. ) Short tones and the base unit length of Morse code communication. \n Dah playMorseWord('-')}> (- ) Long tones, each the length of three dits.\n
\n\n
Spacing \n
The spacing between dits and dahs matters in Morse code. Spacing of various lengths signify different things. \n\n Intra-character Spacing A letter in Morse code can be made up of multiple dits and dahs. The spaces between the dits and dahs that make up a single letter are each the length of one dit. E.g., three dits, each separated by one-dit-long spaces, is an \"S\". (... ) playMorseWord('...')}> \n\n Inter-character Spacing The space between consecutive letters is three dits long. E.g., three dits, each separated by a three-dit-long spaces is \"EEE\". (. . . ) playMorseWord('. . .')}> \n \n Inter-word Spacing The space between words is seven dits long. E.g., three dits, each separated by seven-dit-long spaces (denoted by a forward slash in this example: ././. ), is \"E E E\". playMorseWord('././.')}> \n
\n\n
Speed \n
\n The rate of communication is increased or decreased by adjusting the length of the dits, which in turn adjusts the length of dahs and spaces. Adjust the WPM (Words Per Minute) in the Options section to adjust the speed.\n
\n\n
Telegraph Key Types \n
The instrument used to send Morse code is called the key.
\n \n
\n
Straight Keys use a single button and generate tones when pressed down. Straight keys require greater accuracy as the length of dits, dahs, and spacing is completely under manual control.
\n
\n
Electronic Keys use paddles that automatically generate dits and dahs when pressed. The Electronic Keyer used here is an Iambic keyer that uses two paddles–left paddle for dits, right paddle for dahs. Pressing both paddles simultaneously automatically alternates between dit and dah. Switch between the two paddles at the appropriate times to build letters in Morse code.
\n\n
Check out this video for a demonstration of the difference between Straight and Electronic keys.
\n
\n )\n})","/* eslint-disable array-callback-return */\nimport React from \"react\"\nimport morseCode from '../data/morse-code.json'\nimport useMorsePlayer from \"../hooks/useMorsePlayer\";\n\nexport default React.memo(function Legend() {\n\n const { playMorseWord } = useMorsePlayer()\n\n function handleClick(e) {\n e.preventDefault()\n\n let word = e.target.innerText\n\n if (e.target.className === 'alpha') {\n word = convertWordToMorse(word)\n }\n playMorseWord(word)\n }\n\n function convertWordToMorse(word) {\n let morse = ''\n for (let i in word) {\n morse += morseCode[word[i].toLowerCase()]\n morse += ' '\n }\n return morse\n }\n\n const numbers = Object.keys(morseCode).map((morse, index) =>\n {\n if (index < 10) {\n return (\n \n {morse.toUpperCase()} \n {morseCode[morse]} \n \n )\n }\n }\n )\n const letters = Object.keys(morseCode).map((morse, index) =>\n {\n if (index >= 10 && index < 36) {\n return (\n \n {morse.toUpperCase()} \n {morseCode[morse]} \n \n )\n }\n }\n )\n const special = Object.keys(morseCode).map((morse, index) =>\n {\n if (index > 36) {\n return (\n \n {morse.toUpperCase()} \n {morseCode[morse]} \n \n )\n }\n }\n )\n\n return (\n \n
\n Legend\n
\n
\n {letters}\n {numbers}\n {special}\n
\n
\n )\n})","import React, { useState, useEffect } from \"react\"\nimport morseCode from '../data/morse-code.json'\nimport useMorsePlayer from \"../hooks/useMorsePlayer\";\n\nexport default (function PlayMorseInput() {\n\n const { playMorseWord } = useMorsePlayer()\n const [inputValue, setInputValue] = useState('')\n const [morseTranslation, setMorseTranslation] = useState('')\n\n function handleChange(e) {\n e.preventDefault()\n setInputValue(e.target.value)\n }\n\n function handlePlay() {\n playMorseWord(morseTranslation)\n }\n\n function convertWordToMorse(word) {\n let morse = ''\n for (let i in word) {\n morse += morseCode[word[i].toLowerCase()]\n }\n return morse\n }\n\n // Live translation of text into morse code\n useEffect(() => {\n let arr = Array.from(inputValue.trim().toLowerCase())\n let morse = arr.map(item => {\n if (item === ' ') {\n return '/'\n } else {\n let r = convertWordToMorse(item)\n return (r === 'undefined' ? '?' : r + ' ')\n }\n })\n let a = morse.map(i => i.trim()).join(' ').replace(/ \\/ /g,'/').replace(/ \\?/g,'?')\n setMorseTranslation(a)\n\n }, [inputValue])\n\n return (\n \n )\n})","import React, { useState } from \"react\"\nimport Info from \"./Info\"\nimport Legend from \"./Legend\"\nimport PlayMorseInput from \"./PlayMorseInput\"\n\nexport default (function SidebarLeft() {\n\n const [sidebarContent, setSidebarContent] = useState('nav-learn')\n\n // Hide/show sidebar\n function toggleLeft() {\n document.querySelector('.sidebar#left').classList.toggle('hide')\n document.querySelector('#main-interface').classList.toggle('expandLeft')\n }\n\n // Handle sidebar navigation selection\n function navClicked(e) {\n if (e.target.id === 'nav-learn') {\n setSidebarContent('nav-learn')\n } else if (e.target.id === 'nav-legend') {\n setSidebarContent('nav-legend')\n } else {\n setSidebarContent('nav-play')\n }\n \n let navItems = document.querySelector(\".navbar\").childNodes\n navItems.forEach(item => {\n if (item.id === e.target.id) {\n item.classList.add('selected')\n } else {\n item.classList.remove('selected')\n }\n })\n }\n\n return (\n \n \n
\n )\n\n})\n","import React, {useContext} from \"react\"\nimport { FrequencyContext } from \"../contexts/frequencyContext\";\n\nexport default React.memo(function FrequencyPicker(props) {\n\n const {frequency, setFrequency} = useContext(FrequencyContext)\n\n const minFreq = 300\n const maxFreq = 1500\n\n function handleChange(e) {\n if (Number(e.target.value) > maxFreq) {\n setFrequency(maxFreq)\n } else if (Number(e.target.value) < minFreq) {\n setFrequency(minFreq)\n } else {\n setFrequency(Number(e.target.value))\n }\n }\n\n function increment() {\n setFrequency(prevFreq => {\n if (prevFreq + 10 <= maxFreq) {\n return (prevFreq + 10)\n } else {\n return maxFreq\n }\n })\n }\n\n function decrement() {\n setFrequency(prevFreq => {\n if (prevFreq - 10 >= minFreq) {\n return (prevFreq - 10)\n } else {\n return minFreq\n }\n })\n }\n \n return (\n \n
\n Frequency ({minFreq}-{maxFreq}) \n
\n
\n \n \n \n
\n
\n )\n})","import React, {useContext} from 'react';\nimport './css/App.css';\n\nimport { GameModeContext } from \"./contexts/gameModeContext\"\nimport { MorseBufferContextProvider } from \"./contexts/morseBufferContext\"\nimport { WordFeederContextProvider } from './contexts/wordFeederContext';\nimport { WordListPickerContextProvider } from './contexts/wordListPickerContext';\nimport { GameClockContextProvider } from './contexts/gameClockContext';\nimport { WPMContextProvider } from './contexts/wpmContext';\nimport { FrequencyContextProvider } from './contexts/frequencyContext';\nimport { KeyTypeContextProvider } from './contexts/keyTypeContext';\nimport { ChallengeContextProvider } from './contexts/challengeContext';\n\nimport PracticeMode from './app-modes/PracticeMode';\nimport ChallengeMode from './app-modes/ChallengeMode'\n\nimport ModePicker from './components/ModePicker'\nimport KeyTypePicker from './components/KeyTypePicker'\nimport Header from './components/Header';\nimport WordsPerMinute from \"./components/WordsPerMinute\"\nimport MorseButtons from './components/MorseButtons'\nimport Footer from './components/Footer';\nimport ChallengeOverlay from './components/ChallengeOverlay';\nimport SidebarLeft from './components/SidebarLeft';\nimport FrequencyPicker from './components/FrequencyPicker';\n\nexport default React.memo(function App() {\n\n const {gameMode} = useContext(GameModeContext)\n\n return (\n <>\n \n \n
\n \n \n \n \n \n \n \n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n {gameMode === 'practice' &&
}\n {gameMode === 'challenge' &&\n <>\n
\n
\n >\n }\n
\n
\n \n \n \n \n \n \n \n \n
\n \n >\n );\n})","// This optional code is used to register a service worker.\n// register() is not called by default.\n\n// This lets the app load faster on subsequent visits in production, and gives\n// it offline capabilities. However, it also means that developers (and users)\n// will only see deployed updates on subsequent visits to a page, after all the\n// existing tabs open on the page have been closed, since previously cached\n// resources are updated in the background.\n\n// To learn more about the benefits of this model and instructions on how to\n// opt-in, read https://bit.ly/CRA-PWA\n\nconst isLocalhost = Boolean(\n window.location.hostname === 'localhost' ||\n // [::1] is the IPv6 localhost address.\n window.location.hostname === '[::1]' ||\n // 127.0.0.0/8 are considered localhost for IPv4.\n window.location.hostname.match(\n /^127(?:\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/\n )\n);\n\nexport function register(config) {\n if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {\n // The URL constructor is available in all browsers that support SW.\n const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);\n if (publicUrl.origin !== window.location.origin) {\n // Our service worker won't work if PUBLIC_URL is on a different origin\n // from what our page is served on. This might happen if a CDN is used to\n // serve assets; see https://github.com/facebook/create-react-app/issues/2374\n return;\n }\n\n window.addEventListener('load', () => {\n const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;\n\n if (isLocalhost) {\n // This is running on localhost. Let's check if a service worker still exists or not.\n checkValidServiceWorker(swUrl, config);\n\n // Add some additional logging to localhost, pointing developers to the\n // service worker/PWA documentation.\n navigator.serviceWorker.ready.then(() => {\n console.log(\n 'This web app is being served cache-first by a service ' +\n 'worker. To learn more, visit https://bit.ly/CRA-PWA'\n );\n });\n } else {\n // Is not localhost. Just register service worker\n registerValidSW(swUrl, config);\n }\n });\n }\n}\n\nfunction registerValidSW(swUrl, config) {\n navigator.serviceWorker\n .register(swUrl)\n .then(registration => {\n registration.onupdatefound = () => {\n const installingWorker = registration.installing;\n if (installingWorker == null) {\n return;\n }\n installingWorker.onstatechange = () => {\n if (installingWorker.state === 'installed') {\n if (navigator.serviceWorker.controller) {\n // At this point, the updated precached content has been fetched,\n // but the previous service worker will still serve the older\n // content until all client tabs are closed.\n console.log(\n 'New content is available and will be used when all ' +\n 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'\n );\n\n // Execute callback\n if (config && config.onUpdate) {\n config.onUpdate(registration);\n }\n } else {\n // At this point, everything has been precached.\n // It's the perfect time to display a\n // \"Content is cached for offline use.\" message.\n console.log('Content is cached for offline use.');\n\n // Execute callback\n if (config && config.onSuccess) {\n config.onSuccess(registration);\n }\n }\n }\n };\n };\n })\n .catch(error => {\n console.error('Error during service worker registration:', error);\n });\n}\n\nfunction checkValidServiceWorker(swUrl, config) {\n // Check if the service worker can be found. If it can't reload the page.\n fetch(swUrl, {\n headers: { 'Service-Worker': 'script' }\n })\n .then(response => {\n // Ensure service worker exists, and that we really are getting a JS file.\n const contentType = response.headers.get('content-type');\n if (\n response.status === 404 ||\n (contentType != null && contentType.indexOf('javascript') === -1)\n ) {\n // No service worker found. Probably a different app. Reload the page.\n navigator.serviceWorker.ready.then(registration => {\n registration.unregister().then(() => {\n window.location.reload();\n });\n });\n } else {\n // Service worker found. Proceed as normal.\n registerValidSW(swUrl, config);\n }\n })\n .catch(() => {\n console.log(\n 'No internet connection found. App is running in offline mode.'\n );\n });\n}\n\nexport function unregister() {\n if ('serviceWorker' in navigator) {\n navigator.serviceWorker.ready.then(registration => {\n registration.unregister();\n });\n }\n}\n","import React from 'react';\nimport ReactDOM from 'react-dom';\nimport './index.css';\nimport App from './App';\nimport * as serviceWorker from './serviceWorker';\nimport {GameModeContextProvider} from \"./contexts/gameModeContext\"\n\n\nReactDOM.render(\n \n \n \n\n , document.getElementById('root'));\n\n// If you want your app to work offline and load faster, you can change\n// unregister() to register() below. Note this comes with some pitfalls.\n// Learn more about service workers: https://bit.ly/CRA-PWA\nserviceWorker.unregister();\n"],"sourceRoot":""}
\ No newline at end of file