Repository: bradtraversy/vanillawebprojects Branch: master Commit: adc66a181a67 Files: 85 Total size: 137.8 KB Directory structure: gitextract_u4ai9j3w/ ├── breakout-game/ │ ├── README.md │ ├── index.html │ ├── plan.txt │ ├── script.js │ └── style.css ├── custom-video-player/ │ ├── README.md │ ├── css/ │ │ ├── progress.css │ │ └── style.css │ ├── index.html │ └── script.js ├── dom-array-methods/ │ ├── index.html │ ├── readme.md │ ├── script.js │ └── style.css ├── exchange-rate/ │ ├── README.md │ ├── index.html │ ├── script.js │ └── style.css ├── expense-tracker/ │ ├── index.html │ ├── readme.md │ ├── script.js │ └── style.css ├── form-validator/ │ ├── index.html │ ├── readme.md │ ├── script.js │ └── style.css ├── hangman/ │ ├── README.md │ ├── index.html │ ├── script.js │ └── style.css ├── infinite_scroll_blog/ │ ├── index.html │ ├── readme.md │ ├── script.js │ └── style.css ├── lyrics-search/ │ ├── index.html │ ├── readme.md │ ├── script.js │ └── style.css ├── meal-finder/ │ ├── index.html │ ├── readme.md │ ├── script.js │ └── style.css ├── memory-cards/ │ ├── index.html │ ├── readme.md │ ├── script.js │ └── style.css ├── modal-menu-slider/ │ ├── README.md │ ├── index.html │ ├── script.js │ └── style.css ├── movie-seat-booking/ │ ├── README.md │ ├── index.html │ ├── script.js │ └── style.css ├── music-player/ │ ├── index.html │ ├── readme.md │ ├── script.js │ └── style.css ├── new-year-countdown/ │ ├── README.md │ ├── index.html │ ├── script.js │ └── style.css ├── product-filtering/ │ ├── index.html │ └── script.js ├── readme.md ├── relaxer-app/ │ ├── README.md │ ├── index.html │ ├── script.js │ └── style.css ├── sortable-list/ │ ├── README.md │ ├── index.html │ ├── script.js │ └── style.css ├── speak-number-guess/ │ ├── index.html │ ├── readme.md │ ├── script.js │ └── style.css ├── speech-text-reader/ │ ├── README.md │ ├── index.html │ ├── script.js │ └── style.css └── typing-game/ ├── index.html ├── readme.md ├── script.js └── style.css ================================================ FILE CONTENTS ================================================ ================================================ FILE: breakout-game/README.md ================================================ ## Breakout! Game Game where you control a paddle with the arrow keys to bounce a ball up to break bricks. This app uses the HTML5 canvas element and API ## Project Specifications - Draw elements on canvas - Use canvas paths to draw shapes - Add animation with requestAnimationFrame(cb) - Move paddle on arrow key press - Add collision detection - Keep score - Add rules button with slider ================================================ FILE: breakout-game/index.html ================================================ Breakout!

Breakout!

How To Play:

Use your right and left keys to move the paddle to bounce the ball up and break the blocks.

If you miss the ball, your score and the blocks will reset.

================================================ FILE: breakout-game/plan.txt ================================================ 1. Create canvas context 2. Create and draw ball 3. Create and draw paddle 4. Create bricks 5. Draw score 6. Add update() - Animate - requestAnimationFrame(cb) 7. Move paddle 8. Keyboard event handlers to move paddle 9. Move ball 10. Add wall bounderies 11. Increase score when bricks break 12. Lose - redraw bricks, reset score ================================================ FILE: breakout-game/script.js ================================================ const rulesBtn = document.getElementById('rules-btn'); const closeBtn = document.getElementById('close-btn'); const rules = document.getElementById('rules'); const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); let score = 0; const brickRowCount = 9; const brickColumnCount = 5; const delay = 500; //delay to reset the game // Create ball props const ball = { x: canvas.width / 2, y: canvas.height / 2, size: 10, speed: 4, dx: 4, dy: -4, visible: true }; // Create paddle props const paddle = { x: canvas.width / 2 - 40, y: canvas.height - 20, w: 80, h: 10, speed: 8, dx: 0, visible: true }; // Create brick props const brickInfo = { w: 70, h: 20, padding: 10, offsetX: 45, offsetY: 60, visible: true }; // Create bricks const bricks = []; for (let i = 0; i < brickRowCount; i++) { bricks[i] = []; for (let j = 0; j < brickColumnCount; j++) { const x = i * (brickInfo.w + brickInfo.padding) + brickInfo.offsetX; const y = j * (brickInfo.h + brickInfo.padding) + brickInfo.offsetY; bricks[i][j] = { x, y, ...brickInfo }; } } // Draw ball on canvas function drawBall() { ctx.beginPath(); ctx.arc(ball.x, ball.y, ball.size, 0, Math.PI * 2); ctx.fillStyle = ball.visible ? '#0095dd' : 'transparent'; ctx.fill(); ctx.closePath(); } // Draw paddle on canvas function drawPaddle() { ctx.beginPath(); ctx.rect(paddle.x, paddle.y, paddle.w, paddle.h); ctx.fillStyle = paddle.visible ? '#0095dd' : 'transparent'; ctx.fill(); ctx.closePath(); } // Draw score on canvas function drawScore() { ctx.font = '20px Arial'; ctx.fillText(`Score: ${score}`, canvas.width - 100, 30); } // Draw bricks on canvas function drawBricks() { bricks.forEach(column => { column.forEach(brick => { ctx.beginPath(); ctx.rect(brick.x, brick.y, brick.w, brick.h); ctx.fillStyle = brick.visible ? '#0095dd' : 'transparent'; ctx.fill(); ctx.closePath(); }); }); } // Move paddle on canvas function movePaddle() { paddle.x += paddle.dx; // Wall detection if (paddle.x + paddle.w > canvas.width) { paddle.x = canvas.width - paddle.w; } if (paddle.x < 0) { paddle.x = 0; } } // Move ball on canvas function moveBall() { ball.x += ball.dx; ball.y += ball.dy; // Wall collision (right/left) if (ball.x + ball.size > canvas.width || ball.x - ball.size < 0) { ball.dx *= -1; // ball.dx = ball.dx * -1 } // Wall collision (top/bottom) if (ball.y + ball.size > canvas.height || ball.y - ball.size < 0) { ball.dy *= -1; } // console.log(ball.x, ball.y); // Paddle collision if ( ball.x - ball.size > paddle.x && ball.x + ball.size < paddle.x + paddle.w && ball.y + ball.size > paddle.y ) { ball.dy = -ball.speed; } // Brick collision bricks.forEach(column => { column.forEach(brick => { if (brick.visible) { if ( ball.x - ball.size > brick.x && // left brick side check ball.x + ball.size < brick.x + brick.w && // right brick side check ball.y + ball.size > brick.y && // top brick side check ball.y - ball.size < brick.y + brick.h // bottom brick side check ) { ball.dy *= -1; brick.visible = false; increaseScore(); } } }); }); // Hit bottom wall - Lose if (ball.y + ball.size > canvas.height) { showAllBricks(); score = 0; } } // Increase score function increaseScore() { score++; if (score % (brickRowCount * brickColumnCount) === 0) { ball.visible = false; paddle.visible = false; //After 0.5 sec restart the game setTimeout(function () { showAllBricks(); score = 0; paddle.x = canvas.width / 2 - 40; paddle.y = canvas.height - 20; ball.x = canvas.width / 2; ball.y = canvas.height / 2; ball.visible = true; paddle.visible = true; },delay) } } // Make all bricks appear function showAllBricks() { bricks.forEach(column => { column.forEach(brick => (brick.visible = true)); }); } // Draw everything function draw() { // clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height); drawBall(); drawPaddle(); drawScore(); drawBricks(); } // Update canvas drawing and animation function update() { movePaddle(); moveBall(); // Draw everything draw(); requestAnimationFrame(update); } update(); // Keydown event function keyDown(e) { if (e.key === 'Right' || e.key === 'ArrowRight') { paddle.dx = paddle.speed; } else if (e.key === 'Left' || e.key === 'ArrowLeft') { paddle.dx = -paddle.speed; } } // Keyup event function keyUp(e) { if ( e.key === 'Right' || e.key === 'ArrowRight' || e.key === 'Left' || e.key === 'ArrowLeft' ) { paddle.dx = 0; } } // Keyboard event handlers document.addEventListener('keydown', keyDown); document.addEventListener('keyup', keyUp); // Rules and close event handlers rulesBtn.addEventListener('click', () => rules.classList.add('show')); closeBtn.addEventListener('click', () => rules.classList.remove('show')); ================================================ FILE: breakout-game/style.css ================================================ * { box-sizing: border-box; } body { background-color: #0095dd; display: flex; flex-direction: column; align-items: center; justify-content: center; font-family: Arial, Helvetica, sans-serif; min-height: 100vh; margin: 0; } h1 { font-size: 45px; color: #fff; } canvas { background: #f0f0f0; display: block; border-radius: 5px; } .btn { cursor: pointer; border: 0; padding: 10px 20px; background: #000; color: #fff; border-radius: 5px; } .btn:focus { outline: 0; } .btn:hover { background: #222; } .btn:active { transform: scale(0.98); } .rules-btn { position: absolute; top: 30px; left: 30px; } .rules { position: absolute; top: 0; left: 0; background: #333; color: #fff; min-height: 100vh; width: 400px; padding: 20px; line-height: 1.5; transform: translateX(-400px); transition: transform 1s ease-in-out; } .rules.show { transform: translateX(0); } ================================================ FILE: custom-video-player/README.md ================================================ ## Custom Video Player Custom video player using the HTML5 video element and it's JavaScript API with a custom design ## Project Specifications - Display custom video player styled with CSS - Play/pause - Stop - Video progress bar - Set progress bar time - Display time in mins and seconds ================================================ FILE: custom-video-player/css/progress.css ================================================ /* SOURCE: https://css-tricks.com/styling-cross-browser-compatible-range-inputs-css/ */ input[type='range'] { -webkit-appearance: none; /* Hides the slider so that custom slider can be made */ width: 100%; /* Specific width is required for Firefox. */ background: transparent; /* Otherwise white in Chrome */ } input[type='range']::-webkit-slider-thumb { -webkit-appearance: none; } input[type='range']:focus { outline: none; /* Removes the blue border. You should probably do some kind of focus styling for accessibility reasons though. */ } input[type='range']::-ms-track { width: 100%; cursor: pointer; /* Hides the slider so custom styles can be added */ background: transparent; border-color: transparent; color: transparent; } /* Special styling for WebKit/Blink */ input[type='range']::-webkit-slider-thumb { -webkit-appearance: none; border: 1px solid #000000; height: 36px; width: 16px; border-radius: 3px; background: #ffffff; cursor: pointer; margin-top: -14px; /* You need to specify a margin in Chrome, but in Firefox and IE it is automatic */ box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d; /* Add cool effects to your sliders! */ } /* All the same stuff for Firefox */ input[type='range']::-moz-range-thumb { box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d; border: 1px solid #000000; height: 36px; width: 16px; border-radius: 3px; background: #ffffff; cursor: pointer; } /* All the same stuff for IE */ input[type='range']::-ms-thumb { box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d; border: 1px solid #000000; height: 36px; width: 16px; border-radius: 3px; background: #ffffff; cursor: pointer; } input[type='range']::-webkit-slider-runnable-track { width: 100%; height: 8.4px; cursor: pointer; box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d; background: #3071a9; border-radius: 1.3px; border: 0.2px solid #010101; } input[type='range']:focus::-webkit-slider-runnable-track { background: #367ebd; } input[type='range']::-moz-range-track { width: 100%; height: 8.4px; cursor: pointer; box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d; background: #3071a9; border-radius: 1.3px; border: 0.2px solid #010101; } input[type='range']::-ms-track { width: 100%; height: 8.4px; cursor: pointer; background: transparent; border-color: transparent; border-width: 16px 0; color: transparent; } input[type='range']::-ms-fill-lower { background: #2a6495; border: 0.2px solid #010101; border-radius: 2.6px; box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d; } input[type='range']:focus::-ms-fill-lower { background: #3071a9; } input[type='range']::-ms-fill-upper { background: #3071a9; border: 0.2px solid #010101; border-radius: 2.6px; box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d; } input[type='range']:focus::-ms-fill-upper { background: #367ebd; } ================================================ FILE: custom-video-player/css/style.css ================================================ @import url('https://fonts.googleapis.com/css?family=Questrial&display=swap'); * { box-sizing: border-box; } body { font-family: 'Questrial', sans-serif; background-color: #666; display: flex; flex-direction: column; align-items: center; justify-content: center; max-height: 100vh; margin: 0; } h1 { color: #fff; } .screen { cursor: pointer; width: 60%; background-color: #000 !important; border-top-left-radius: 10px; border-top-right-radius: 10px; } .controls { background: #333; color: #fff; width: 60%; border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; display: flex; justify-content: center; align-items: center; padding: 10px; } .controls .btn { border: 0; background: transparent; cursor: pointer; } .controls .fa-play { color: #28a745; } .controls .fa-stop { color: #dc3545; } .controls .fa-pause { color: #fff; } .controls .timestamp { color: #fff; font-weight: bold; margin-left: 10px; } .btn:focus { outline: 0; } @media (max-width: 800px) { .screen, .controls { width: 90%; } } ================================================ FILE: custom-video-player/index.html ================================================ Custom Video Player

Custom Video Player

00:00
================================================ FILE: custom-video-player/script.js ================================================ const video = document.getElementById('video'); const play = document.getElementById('play'); const stop = document.getElementById('stop'); const progress = document.getElementById('progress'); const timestamp = document.getElementById('timestamp'); // Play & pause video function toggleVideoStatus() { if (video.paused) { video.play(); } else { video.pause(); } } // update play/pause icon function updatePlayIcon() { if (video.paused) { play.innerHTML = ''; } else { play.innerHTML = ''; } } // Update progress & timestamp function updateProgress() { progress.value = (video.currentTime / video.duration) * 100; // Get the minutes let mins = Math.floor(video.currentTime / 60); if(mins < video.duration){ mins = '0' + String(mins); } // Get Seconds let secs = Math.floor(video.currentTime % 60); if(secs < video.duration){ secs = '0' + String(secs); } timestamp.innerHTML = `${mins}:${secs}`; } // Set video time to progress function setVideoProgress() { video.currentTime = (+progress.value * video.duration) / 100; } // Stop video function stopVideo() { video.currentTime = 0; video.pause(); } // Event listeners video.addEventListener('click', toggleVideoStatus); video.addEventListener('pause', updatePlayIcon); video.addEventListener('play', updatePlayIcon); video.addEventListener('timeupdate', updateProgress); play.addEventListener('click', toggleVideoStatus); stop.addEventListener('click', stopVideo); progress.addEventListener('change', setVideoProgress); ================================================ FILE: dom-array-methods/index.html ================================================ DOM Array Methods

DOM Array Methods

Person Wealth

================================================ FILE: dom-array-methods/readme.md ================================================ ## DOM Array Methods Project to teach high order array methods and DOM manipulation ## Project Specifications - Fetch random users from the [randomuser.me](https://randomuser.me) API - Use forEach() to loop and output user/wealth - Use map() to double wealth - Use filter() to filter only millionaires - Use sort() to sort by wealth - Use reduce() to add all wealth ================================================ FILE: dom-array-methods/script.js ================================================ const main = document.getElementById('main'); const addUserBtn = document.getElementById('add-user'); const doubleBtn = document.getElementById('double'); const showMillionairesBtn = document.getElementById('show-millionaires'); const sortBtn = document.getElementById('sort'); const calculateWealthBtn = document.getElementById('calculate-wealth'); let data = []; getRandomUser(); getRandomUser(); getRandomUser(); // Fetch random user and add money async function getRandomUser() { const res = await fetch('https://randomuser.me/api'); const data = await res.json(); const user = data.results[0]; const newUser = { name: `${user.name.first} ${user.name.last}`, money: Math.floor(Math.random() * 1000000) }; addData(newUser); } // Double eveyones money function doubleMoney() { data = data.map(user => { return { ...user, money: user.money * 2 }; }); updateDOM(); } // Sort users by richest function sortByRichest() { console.log(123); data.sort((a, b) => b.money - a.money); updateDOM(); } // Filter only millionaires function showMillionaires() { data = data.filter(user => user.money > 1000000); updateDOM(); } // Calculate the total wealth function calculateWealth() { const wealth = data.reduce((acc, user) => (acc += user.money), 0); const wealthEl = document.createElement('div'); wealthEl.innerHTML = `

Total Wealth: ${formatMoney( wealth )}

`; main.appendChild(wealthEl); } // Add new obj to data arr function addData(obj) { data.push(obj); updateDOM(); } // Update DOM function updateDOM(providedData = data) { // Clear main div main.innerHTML = '

Person Wealth

'; providedData.forEach(item => { const element = document.createElement('div'); element.classList.add('person'); element.innerHTML = `${item.name} ${formatMoney( item.money )}`; main.appendChild(element); }); } // Format number as money - https://stackoverflow.com/questions/149055/how-to-format-numbers-as-currency-string function formatMoney(number) { return '$' + number.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,'); } // Event listeners addUserBtn.addEventListener('click', getRandomUser); doubleBtn.addEventListener('click', doubleMoney); sortBtn.addEventListener('click', sortByRichest); showMillionairesBtn.addEventListener('click', showMillionaires); calculateWealthBtn.addEventListener('click', calculateWealth); ================================================ FILE: dom-array-methods/style.css ================================================ * { box-sizing: border-box; } body { background: #f4f4f4; font-family: Arial, Helvetica, sans-serif; display: flex; flex-direction: column; align-items: center; min-height: 100vh; margin: 0; } .container { display: flex; padding: 20px; margin: 0 auto; max-width: 100%; width: 800px; } aside { padding: 10px 20px; width: 250px; border-right: 1px solid #111; } button { cursor: pointer; background-color: #fff; border: solid 1px #111; border-radius: 5px; display: block; width: 100%; padding: 10px; margin-bottom: 20px; font-weight: bold; font-size: 14px; } main { flex: 1; padding: 10px 20px; } h2 { border-bottom: 1px solid #111; padding-bottom: 10px; display: flex; justify-content: space-between; font-weight: 300; margin: 0 0 20px; } h3 { background-color: #fff; border-bottom: 1px solid #111; padding: 10px; display: flex; justify-content: space-between; font-weight: 300; margin: 20px 0 0; } .person { display: flex; justify-content: space-between; font-size: 20px; margin-bottom: 10px; } ================================================ FILE: exchange-rate/README.md ================================================ ## Exchange Rate Select countries to get the exchange rate for a specific amount ## Project Specifications - Display UI with 2 select lists for countries and 2 inputs for amounts - Fetch exchange rates from API (https://api.exchangerate-api.com) - Display the values for both countries - Update values on amount change - Swap country rates ================================================ FILE: exchange-rate/index.html ================================================ Exchange Rate Calculator

Exchange Rate Calculator

Choose the currency and the amounts to get the exchange rate

================================================ FILE: exchange-rate/script.js ================================================ const currencyEl_one = document.getElementById('currency-one'); const amountEl_one = document.getElementById('amount-one'); const currencyEl_two = document.getElementById('currency-two'); const amountEl_two = document.getElementById('amount-two'); const rateEl = document.getElementById('rate'); const swap = document.getElementById('swap'); function calculate() { const currency_one = currencyEl_one.value; const currency_two = currencyEl_two.value; fetch("https://open.exchangerate-api.com/v6/latest") .then(res => res.json()) .then(data => { // console.log(data); const rate = data.rates[currency_two] / data.rates[currency_one]; rateEl.innerText = `1 ${currency_one} = ${rate} ${currency_two}`; amountEl_two.value = (amountEl_one.value * (rate)).toFixed(2); }); } // Event Listener currencyEl_one.addEventListener('change', calculate); amountEl_one.addEventListener('input', calculate); currencyEl_two.addEventListener('change', calculate); amountEl_two.addEventListener('input', calculate); swap.addEventListener('click', () => { const temp = currencyEl_one.value; currencyEl_one.value = currencyEl_two.value; currencyEl_two.value = temp; calculate(); }); calculate(); ================================================ FILE: exchange-rate/style.css ================================================ :root { --primary-color: #5fbaa7; } * { box-sizing: border-box; } body { background-color: #f4f4f4; font-family: Arial, Helvetica, sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; padding: 20px; } h1 { color: var(--primary-color); } p { text-align: center; } .btn { color: #fff; background: var(--primary-color); cursor: pointer; border-radius: 5px; font-size: 12px; padding: 5px 12px; } .money-img { width: 150px; } .currency { padding: 40px 0; display: flex; align-items: center; justify-content: space-between; } .currency select { padding: 10px 20px 10px 10px; -moz-appearance: none; -webkit-appearance: none; appearance: none; border: 1px solid #dedede; font-size: 16px; /* You may not need these following lines. The arrow did not show for me on MacOS/Chrome so I added it. Just remove it if you would like */ background: transparent; background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%20000002%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'); background-position: right 10px top 50%, 0, 0; background-size: 12px auto, 100%; background-repeat: no-repeat; } .currency input { border: 0; background: transparent; font-size: 30px; text-align: right; } .swap-rate-container { display: flex; align-items: center; justify-content: space-between; } .rate { color: var(--primary-color); font-size: 14px; padding: 0 10px; } select:focus, input:focus, button:focus { outline: 0; } @media (max-width: 600px) { .currency input { width: 200px; } } ================================================ FILE: expense-tracker/index.html ================================================ Expense Tracker

Expense Tracker

Your Balance

$0.00

Income

+$0.00

Expense

-$0.00

History

Add new transaction

================================================ FILE: expense-tracker/readme.md ================================================ ## Expense Tracker Keep track of income and expenses. Add and remove items and save to local storage ## Project Specifications - Create UI for project - Display transaction items in DOM - Show balance, expense and income totals - Add new transation and reflect in total - Delete items from DOM - Persist to local storage ================================================ FILE: expense-tracker/script.js ================================================ const balance = document.getElementById('balance'); const money_plus = document.getElementById('money-plus'); const money_minus = document.getElementById('money-minus'); const list = document.getElementById('list'); const form = document.getElementById('form'); const text = document.getElementById('text'); const amount = document.getElementById('amount'); // const dummyTransactions = [ // { id: 1, text: 'Flower', amount: -20 }, // { id: 2, text: 'Salary', amount: 300 }, // { id: 3, text: 'Book', amount: -10 }, // { id: 4, text: 'Camera', amount: 150 } // ]; const localStorageTransactions = JSON.parse( localStorage.getItem('transactions') ); let transactions = localStorage.getItem('transactions') !== null ? localStorageTransactions : []; // Add transaction function addTransaction(e) { e.preventDefault(); if (text.value.trim() === '' || amount.value.trim() === '') { alert('Please add a text and amount'); } else { const transaction = { id: generateID(), text: text.value, amount: +amount.value }; transactions.push(transaction); addTransactionDOM(transaction); updateValues(); updateLocalStorage(); text.value = ''; amount.value = ''; } } // Generate random ID function generateID() { return Math.floor(Math.random() * 100000000); } // Add transactions to DOM list function addTransactionDOM(transaction) { // Get sign const sign = transaction.amount < 0 ? '-' : '+'; const item = document.createElement('li'); // Add class based on value item.classList.add(transaction.amount < 0 ? 'minus' : 'plus'); item.innerHTML = ` ${transaction.text} ${sign}${Math.abs( transaction.amount )} `; list.appendChild(item); } // Update the balance, income and expense function updateValues() { const amounts = transactions.map(transaction => transaction.amount); const total = amounts.reduce((acc, item) => (acc += item), 0).toFixed(2); const income = amounts .filter(item => item > 0) .reduce((acc, item) => (acc += item), 0) .toFixed(2); const expense = ( amounts.filter(item => item < 0).reduce((acc, item) => (acc += item), 0) * -1 ).toFixed(2); balance.innerText = `$${total}`; money_plus.innerText = `$${income}`; money_minus.innerText = `$${expense}`; } // Remove transaction by ID function removeTransaction(id) { transactions = transactions.filter(transaction => transaction.id !== id); updateLocalStorage(); init(); } // Update local storage transactions function updateLocalStorage() { localStorage.setItem('transactions', JSON.stringify(transactions)); } // Init app function init() { list.innerHTML = ''; transactions.forEach(addTransactionDOM); updateValues(); } init(); form.addEventListener('submit', addTransaction); ================================================ FILE: expense-tracker/style.css ================================================ @import url('https://fonts.googleapis.com/css?family=Lato&display=swap'); :root { --box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); } * { box-sizing: border-box; } body { background-color: #f7f7f7; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; margin: 0; font-family: 'Lato', sans-serif; font-size: 18px; } .container { margin: 30px auto; width: 400px; } h1 { letter-spacing: 1px; margin: 0; } h3 { border-bottom: 1px solid #bbb; padding-bottom: 10px; margin: 40px 0 10px; } h4 { margin: 0; text-transform: uppercase; } .inc-exp-container { background-color: #fff; box-shadow: var(--box-shadow); padding: 20px; display: flex; justify-content: space-between; margin: 20px 0; } .inc-exp-container > div { flex: 1; text-align: center; } .inc-exp-container > div:first-of-type { border-right: 1px solid #dedede; } .money { font-size: 20px; letter-spacing: 1px; margin: 5px 0; } .money.plus { color: #2ecc71; } .money.minus { color: #c0392b; } label { display: inline-block; margin: 10px 0; } input[type='text'], input[type='number'] { border: 1px solid #dedede; border-radius: 2px; display: block; font-size: 16px; padding: 10px; width: 100%; } .btn { cursor: pointer; background-color: #9c88ff; box-shadow: var(--box-shadow); color: #fff; border: 0; display: block; font-size: 16px; margin: 10px 0 30px; padding: 10px; width: 100%; } .btn:focus, .delete-btn:focus { outline: 0; } .list { list-style-type: none; padding: 0; margin-bottom: 40px; } .list li { background-color: #fff; box-shadow: var(--box-shadow); color: #333; display: flex; justify-content: space-between; position: relative; padding: 10px; margin: 10px 0; } .list li.plus { border-right: 5px solid #2ecc71; } .list li.minus { border-right: 5px solid #c0392b; } .delete-btn { cursor: pointer; background-color: #e74c3c; border: 0; color: #fff; font-size: 20px; line-height: 20px; padding: 2px 5px; position: absolute; top: 50%; left: 0; transform: translate(-100%, -50%); opacity: 0; transition: opacity 0.3s ease; } .list li:hover .delete-btn { opacity: 1; } ================================================ FILE: form-validator/index.html ================================================ Form Validator

Register With Us

Error message
Error message
Error message
Error message
================================================ FILE: form-validator/readme.md ================================================ ## Form Validator (Intro Project) Simple client side form validation. Check required, length, email and password match ## Project Specifications - Create form UI - Show error messages under specific inputs - checkRequired() to accept array of inputs - checkLength() to check min and max length - checkEmail() to validate email with regex - checkPasswordsMatch() to match confirm password ================================================ FILE: form-validator/script.js ================================================ const form = document.getElementById('form'); const username = document.getElementById('username'); const email = document.getElementById('email'); const password = document.getElementById('password'); const password2 = document.getElementById('password2'); // Show input error message function showError(input, message) { const formControl = input.parentElement; formControl.className = 'form-control error'; const small = formControl.querySelector('small'); small.innerText = message; } // Show success outline function showSuccess(input) { const formControl = input.parentElement; formControl.className = 'form-control success'; } // Check email is valid function checkEmail(input) { const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; if (re.test(input.value.trim())) { showSuccess(input); } else { showError(input, 'Email is not valid'); } } // Check required fields function checkRequired(inputArr) { let isRequired = false; inputArr.forEach(function(input) { if (input.value.trim() === '') { showError(input, `${getFieldName(input)} is required`); isRequired = true; } else { showSuccess(input); } }); return isRequired; } // Check input length function checkLength(input, min, max) { if (input.value.length < min) { showError( input, `${getFieldName(input)} must be at least ${min} characters` ); } else if (input.value.length > max) { showError( input, `${getFieldName(input)} must be less than ${max} characters` ); } else { showSuccess(input); } } // Check passwords match function checkPasswordsMatch(input1, input2) { if (input1.value !== input2.value) { showError(input2, 'Passwords do not match'); } } // Get fieldname function getFieldName(input) { return input.id.charAt(0).toUpperCase() + input.id.slice(1); } // Event listeners form.addEventListener('submit', function(e) { e.preventDefault(); if(checkRequired([username, email, password, password2])){ checkLength(username, 3, 15); checkLength(password, 6, 25); checkEmail(email); checkPasswordsMatch(password, password2); } }); ================================================ FILE: form-validator/style.css ================================================ @import url('https://fonts.googleapis.com/css?family=Open+Sans&display=swap'); :root { --success-color: #2ecc71; --error-color: #e74c3c; } * { box-sizing: border-box; } body { background-color: #f9fafb; font-family: 'Open Sans', sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; } .container { background-color: #fff; border-radius: 5px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); width: 400px; } h2 { text-align: center; margin: 0 0 20px; } .form { padding: 30px 40px; } .form-control { margin-bottom: 10px; padding-bottom: 20px; position: relative; } .form-control label { color: #777; display: block; margin-bottom: 5px; } .form-control input { border: 2px solid #f0f0f0; border-radius: 4px; display: block; width: 100%; padding: 10px; font-size: 14px; } .form-control input:focus { outline: 0; border-color: #777; } .form-control.success input { border-color: var(--success-color); } .form-control.error input { border-color: var(--error-color); } .form-control small { color: var(--error-color); position: absolute; bottom: 0; left: 0; visibility: hidden; } .form-control.error small { visibility: visible; } .form button { cursor: pointer; background-color: #3498db; border: 2px solid #3498db; border-radius: 4px; color: #fff; display: block; font-size: 16px; padding: 10px; margin-top: 20px; width: 100%; } ================================================ FILE: hangman/README.md ================================================ ## Hangman Game Select a letter to figure out a hidden word in a set amount of chances ## Project Specifications - Display hangman pole and figure using SVG - Generate a random word - Display word in UI with correct letters - Display wrong letters - Show notification when select a letter twice - Show popup on win or lose - Play again button to reset game ================================================ FILE: hangman/index.html ================================================ Hangman

Hangman

Find the hidden word - Enter a letter

You have already entered this letter

================================================ FILE: hangman/script.js ================================================ const wordEl = document.getElementById('word'); const wrongLettersEl = document.getElementById('wrong-letters'); const playAgainBtn = document.getElementById('play-button'); const popup = document.getElementById('popup-container'); const notification = document.getElementById('notification-container'); const finalMessage = document.getElementById('final-message'); const finalMessageRevealWord = document.getElementById('final-message-reveal-word'); const figureParts = document.querySelectorAll('.figure-part'); const words = ['application', 'programming', 'interface', 'wizard']; let selectedWord = words[Math.floor(Math.random() * words.length)]; let playable = true; const correctLetters = []; const wrongLetters = []; // Show hidden word function displayWord() { wordEl.innerHTML = ` ${selectedWord .split('') .map( letter => ` ${correctLetters.includes(letter) ? letter : ''} ` ) .join('')} `; const innerWord = wordEl.innerText.replace(/[ \n]/g, ''); if (innerWord === selectedWord) { finalMessage.innerText = 'Congratulations! You won! 😃'; finalMessageRevealWord.innerText = ''; popup.style.display = 'flex'; playable = false; } } // Update the wrong letters function updateWrongLettersEl() { // Display wrong letters wrongLettersEl.innerHTML = ` ${wrongLetters.length > 0 ? '

Wrong

' : ''} ${wrongLetters.map(letter => `${letter}`)} `; // Display parts figureParts.forEach((part, index) => { const errors = wrongLetters.length; if (index < errors) { part.style.display = 'block'; } else { part.style.display = 'none'; } }); // Check if lost if (wrongLetters.length === figureParts.length) { finalMessage.innerText = 'Unfortunately you lost. 😕'; finalMessageRevealWord.innerText = `...the word was: ${selectedWord}`; popup.style.display = 'flex'; playable = false; } } // Show notification function showNotification() { notification.classList.add('show'); setTimeout(() => { notification.classList.remove('show'); }, 2000); } // Keydown letter press window.addEventListener('keydown', e => { if (playable) { if (e.keyCode >= 65 && e.keyCode <= 90) { const letter = e.key.toLowerCase(); if (selectedWord.includes(letter)) { if (!correctLetters.includes(letter)) { correctLetters.push(letter); displayWord(); } else { showNotification(); } } else { if (!wrongLetters.includes(letter)) { wrongLetters.push(letter); updateWrongLettersEl(); } else { showNotification(); } } } } }); // Restart game and play again playAgainBtn.addEventListener('click', () => { playable = true; // Empty arrays correctLetters.splice(0); wrongLetters.splice(0); selectedWord = words[Math.floor(Math.random() * words.length)]; displayWord(); updateWrongLettersEl(); popup.style.display = 'none'; }); displayWord(); ================================================ FILE: hangman/style.css ================================================ * { box-sizing: border-box; } body { background-color: #34495e; color: #fff; font-family: Arial, Helvetica, sans-serif; display: flex; flex-direction: column; align-items: center; height: 80vh; margin: 0; overflow: hidden; } h1 { margin: 20px 0 0; } .game-container { padding: 20px 30px; position: relative; margin: auto; height: 350px; width: 450px; } .figure-container { fill: transparent; stroke: #fff; stroke-width: 4px; stroke-linecap: round; } .figure-part { display: none; } .wrong-letters-container { position: absolute; top: 20px; right: 20px; display: flex; flex-direction: column; text-align: right; } .wrong-letters-container p { margin: 0 0 5px; } .wrong-letters-container span { font-size: 24px; } .word { display: flex; position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%); } .letter { border-bottom: 3px solid #2980b9; display: inline-flex; font-size: 30px; align-items: center; justify-content: center; margin: 0 3px; height: 50px; width: 20px; } .popup-container { background-color: rgba(0, 0, 0, 0.3); position: fixed; top: 0; bottom: 0; left: 0; right: 0; /* display: flex; */ display: none; align-items: center; justify-content: center; } .popup { background: #2980b9; border-radius: 5px; box-shadow: 0 15px 10px 3px rgba(0, 0, 0, 0.1); padding: 20px; text-align: center; } .popup button { cursor: pointer; background-color: #fff; color: #2980b9; border: 0; margin-top: 20px; padding: 12px 20px; font-size: 16px; } .popup button:active { transform: scale(0.98); } .popup button:focus { outline: 0; } .notification-container { background-color: rgba(0, 0, 0, 0.3); border-radius: 10px 10px 0 0; padding: 15px 20px; position: absolute; bottom: -50px; transition: transform 0.3s ease-in-out; } .notification-container p { margin: 0; } .notification-container.show { transform: translateY(-50px); } ================================================ FILE: infinite_scroll_blog/index.html ================================================ My Blog

My Blog

================================================ FILE: infinite_scroll_blog/readme.md ================================================ ## Infinite Scrolling & Filter Display blog posts from [jsonplaceholder](https://jsonplaceholder.typicode.com) and add infinite scroll to fetch posts and also add filter box ## Project Specifications - Create UI & custom CSS loader animation - Fetch initial posts from API and display - Scroll down, show loader and fetch next set of posts - Add filtering for fetched posts ================================================ FILE: infinite_scroll_blog/script.js ================================================ const postsContainer = document.getElementById('posts-container'); const loading = document.querySelector('.loader'); const filter = document.getElementById('filter'); let limit = 5; let page = 1; // Fetch posts from API async function getPosts() { const res = await fetch( `https://jsonplaceholder.typicode.com/posts?_limit=${limit}&_page=${page}` ); const data = await res.json(); return data; } // Show posts in DOM async function showPosts() { const posts = await getPosts(); posts.forEach(post => { const postEl = document.createElement('div'); postEl.classList.add('post'); postEl.innerHTML = `
${post.id}

${post.title}

${post.body}

`; postsContainer.appendChild(postEl); }); } // Show loader & fetch more posts function showLoading() { loading.classList.add('show'); setTimeout(() => { loading.classList.remove('show'); setTimeout(() => { page++; showPosts(); }, 300); }, 1000); } // Filter posts by input function filterPosts(e) { const term = e.target.value.toUpperCase(); const posts = document.querySelectorAll('.post'); posts.forEach(post => { const title = post.querySelector('.post-title').innerText.toUpperCase(); const body = post.querySelector('.post-body').innerText.toUpperCase(); if (title.indexOf(term) > -1 || body.indexOf(term) > -1) { post.style.display = 'flex'; } else { post.style.display = 'none'; } }); } // Show initial posts showPosts(); window.addEventListener('scroll', () => { const { scrollTop, scrollHeight, clientHeight } = document.documentElement; if (scrollHeight - scrollTop === clientHeight) { showLoading(); } }); filter.addEventListener('input', filterPosts); ================================================ FILE: infinite_scroll_blog/style.css ================================================ @import url('https://fonts.googleapis.com/css?family=Roboto&display=swap'); * { box-sizing: border-box; } body { background-color: #296ca8; color: #fff; font-family: 'Roboto', sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; margin: 0; padding-bottom: 100px; } h1 { margin-bottom: 0; text-align: center; } .filter-container { margin-top: 20px; width: 80vw; max-width: 800px; } .filter { width: 100%; padding: 12px; font-size: 16px; } .post { position: relative; background-color: #4992d3; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); border-radius: 3px; padding: 20px; margin: 40px 0; display: flex; width: 80vw; max-width: 800px; } .post .post-title { margin: 0; } .post .post-body { margin: 15px 0 0; line-height: 1.3; } .post .post-info { margin-left: 20px; } .post .number { position: absolute; top: -15px; left: -15px; font-size: 15px; width: 40px; height: 40px; border-radius: 50%; background: #fff; color: #296ca8; display: flex; align-items: center; justify-content: center; padding: 7px 10px; } .loader { opacity: 0; display: flex; position: fixed; bottom: 50px; transition: opacity 0.3s ease-in; } .loader.show { opacity: 1; } .circle { background-color: #fff; width: 10px; height: 10px; border-radius: 50%; margin: 5px; animation: bounce 0.5s ease-in infinite; } .circle:nth-of-type(2) { animation-delay: 0.1s; } .circle:nth-of-type(3) { animation-delay: 0.2s; } @keyframes bounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-10px); } } ================================================ FILE: lyrics-search/index.html ================================================ LyricsSearch

LyricsSearch

Results will be displayed here

================================================ FILE: lyrics-search/readme.md ================================================ ## LyricsSearch App Find songs, artists and lyrics using the [lyrics.ovh](https://lyrics.ovh) API ## Project Specifications - Display UI with song/artist input - Fetch songs/artists and put in DOM - Add pagination - Add get lyrics functionality and display in DOM ================================================ FILE: lyrics-search/script.js ================================================ const form = document.getElementById('form'); const search = document.getElementById('search'); const result = document.getElementById('result'); const more = document.getElementById('more'); const apiURL = 'https://api.lyrics.ovh'; // Search by song or artist async function searchSongs(term) { const res = await fetch(`${apiURL}/suggest/${term}`); const data = await res.json(); showData(data); } // Show song and artist in DOM function showData(data) { result.innerHTML = ` `; if (data.prev || data.next) { more.innerHTML = ` ${ data.prev ? `` : '' } ${ data.next ? `` : '' } `; } else { more.innerHTML = ''; } } // Get prev and next songs async function getMoreSongs(url) { const res = await fetch(`https://cors-anywhere.herokuapp.com/${url}`); const data = await res.json(); showData(data); } // Get lyrics for song async function getLyrics(artist, songTitle) { const res = await fetch(`${apiURL}/v1/${artist}/${songTitle}`); const data = await res.json(); if (data.error) { result.innerHTML = data.error; } else { const lyrics = data.lyrics.replace(/(\r\n|\r|\n)/g, '
'); result.innerHTML = `

${artist} - ${songTitle}

${lyrics} `; } more.innerHTML = ''; } // Event listeners form.addEventListener('submit', e => { e.preventDefault(); const searchTerm = search.value.trim(); if (!searchTerm) { alert('Please type in a search term'); } else { searchSongs(searchTerm); } }); // Get lyrics button click result.addEventListener('click', e => { const clickedEl = e.target; if (clickedEl.tagName === 'BUTTON') { const artist = clickedEl.getAttribute('data-artist'); const songTitle = clickedEl.getAttribute('data-songtitle'); getLyrics(artist, songTitle); } }); ================================================ FILE: lyrics-search/style.css ================================================ * { box-sizing: border-box; } body { background-color: #fff; font-family: Arial, Helvetica, sans-serif; margin: 0; } button { cursor: pointer; } button:active { transform: scale(0.95); } input:focus, button:focus { outline: none; } header { background-image: url('https://images.unsplash.com/photo-1510915361894-db8b60106cb1?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1350&q=80'); background-repeat: no-repeat; background-size: cover; background-position: center center; color: #fff; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 100px 0; position: relative; } header::after { content: ''; position: absolute; top: 0; left: 0; height: 100%; width: 100%; background-color: rgba(0, 0, 0, 0.4); } header * { z-index: 1; } header h1 { margin: 0 0 30px; } form { position: relative; width: 500px; max-width: 100%; } form input { border: 0; border-radius: 50px; font-size: 16px; padding: 15px 30px; width: 100%; } form button { position: absolute; top: 2px; right: 2px; background-color: #e056fd; border: 0; border-radius: 50px; color: #fff; font-size: 16px; padding: 13px 30px; } .btn { background-color: #8d56fd; border: 0; border-radius: 10px; color: #fff; padding: 4px 10px; } ul.songs { list-style-type: none; padding: 0; } ul.songs li { display: flex; align-items: center; justify-content: space-between; margin: 10px 0; } .container { margin: 30px auto; max-width: 100%; width: 500px; } .container h2 { font-weight: 300; } .container p { text-align: center; } .centered { display: flex; justify-content: center; } .centered button { transform: scale(1.3); margin: 15px; } ================================================ FILE: meal-finder/index.html ================================================ Meal Finder

Meal Finder

================================================ FILE: meal-finder/readme.md ================================================ ## Meal Finder App Search and generate random meals from the [themealdb.com](https://www.themealdb.com) API ## Project Specifications - Display UI with form to search and button to generate - Connect to API and get meals - Display meals in DOM with image and hover effect - Click on meal and see the details - Click on generate button and fetch & display a random meal ================================================ FILE: meal-finder/script.js ================================================ const search = document.getElementById('search'), submit = document.getElementById('submit'), random = document.getElementById('random'), mealsEl = document.getElementById('meals'), resultHeading = document.getElementById('result-heading'), single_mealEl = document.getElementById('single-meal'); // Search meal and fetch from API function searchMeal(e) { e.preventDefault(); // Clear single meal single_mealEl.innerHTML = ''; // Get search term const term = search.value; // Check for empty if (term.trim()) { fetch(`https://www.themealdb.com/api/json/v1/1/search.php?s=${term}`) .then(res => res.json()) .then(data => { console.log(data); resultHeading.innerHTML = `

Search results for '${term}':

`; if (data.meals === null) { resultHeading.innerHTML = `

There are no search results. Try again!

`; } else { mealsEl.innerHTML = data.meals .map( meal => `

${meal.strMeal}

${meal.strMeal}

` ) .join(''); } }); // Clear search text search.value = ''; } else { alert('Please enter a search term'); } } // Fetch meal by ID function getMealById(mealID) { fetch(`https://www.themealdb.com/api/json/v1/1/lookup.php?i=${mealID}`) .then(res => res.json()) .then(data => { const meal = data.meals[0]; addMealToDOM(meal); }); } // Fetch random meal from API function getRandomMeal() { // Clear meals and heading mealsEl.innerHTML = ''; resultHeading.innerHTML = ''; fetch(`https://www.themealdb.com/api/json/v1/1/random.php`) .then(res => res.json()) .then(data => { const meal = data.meals[0]; addMealToDOM(meal); }); } // Add meal to DOM function addMealToDOM(meal) { const ingredients = []; for (let i = 1; i <= 20; i++) { if (meal[`strIngredient${i}`]) { ingredients.push( `${meal[`strIngredient${i}`]} - ${meal[`strMeasure${i}`]}` ); } else { break; } } single_mealEl.innerHTML = `

${meal.strMeal}

${meal.strMeal}
${meal.strCategory ? `

${meal.strCategory}

` : ''} ${meal.strArea ? `

${meal.strArea}

` : ''}

${meal.strInstructions}

Ingredients

`; } // Event listeners submit.addEventListener('submit', searchMeal); random.addEventListener('click', getRandomMeal); mealsEl.addEventListener('click', e => { const mealInfo = e.composedPath().find(item => { if (item.classList) { return item.classList.contains('meal-info'); } else { return false; } }); if (mealInfo) { const mealID = mealInfo.getAttribute('data-mealid'); getMealById(mealID); } }); ================================================ FILE: meal-finder/style.css ================================================ * { box-sizing: border-box; } body { background: #2d2013; color: #fff; font-family: Verdana, Geneva, Tahoma, sans-serif; margin: 0; } .container { margin: auto; max-width: 800px; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; } .flex { display: flex; } input, button { border: 1px solid #dedede; border-top-left-radius: 4px; border-bottom-left-radius: 4px; font-size: 14px; padding: 8px 10px; margin: 0; } input[type='text'] { width: 300px; } .search-btn { cursor: pointer; border-left: 0; border-radius: 0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; } .random-btn { cursor: pointer; margin-left: 10px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; } .meals { display: grid; grid-template-columns: repeat(4, 1fr); grid-gap: 20px; margin-top: 20px; } .meal { cursor: pointer; position: relative; height: 180px; width: 180px; text-align: center; } .meal img { width: 100%; height: 100%; border: 4px #fff solid; border-radius: 2px; } .meal-info { position: absolute; top: 0; left: 0; height: 100%; width: 100%; background: rgba(0, 0, 0, 0.7); display: flex; align-items: center; justify-content: center; transition: opacity 0.2s ease-in; opacity: 0; } .meal:hover .meal-info { opacity: 1; } .single-meal { margin: 30px auto; width: 70%; } .single-meal img { width: 300px; margin: 15px; border: 4px #fff solid; border-radius: 2px; } .single-meal-info { margin: 20px; padding: 10px; border: 2px #e09850 dashed; border-radius: 5px; } .single-meal p { margin: 0; letter-spacing: 0.5px; line-height: 1.5; } .single-meal ul { padding-left: 0; list-style-type: none; } .single-meal ul li { border: 1px solid #ededed; border-radius: 5px; background-color: #fff; display: inline-block; color: #2d2013; font-size: 12px; font-weight: bold; padding: 5px; margin: 0 5px 5px 0; } @media (max-width: 800px) { .meals { grid-template-columns: repeat(3, 1fr); } } @media (max-width: 700px) { .meals { grid-template-columns: repeat(2, 1fr); } .meal { height: 200px; width: 200px; } } @media (max-width: 500px) { input[type='text'] { width: 100%; } .meals { grid-template-columns: 1fr; } .meal { height: 300px; width: 300px; } } ================================================ FILE: memory-cards/index.html ================================================ Memory Cards

Memory Cards

Add New Card

================================================ FILE: memory-cards/readme.md ================================================ ## Memory Cards Flash card app for learning. Display, add and remove memory cards with questions and answers ## Project Specifications - Create flip cards using CSS - Create "Add new card" overlay with form - Display question cards and flip for answer - View prev and next cards - Add new cards to local storage - Clear all cards from local storage ================================================ FILE: memory-cards/script.js ================================================ const cardsContainer = document.getElementById('cards-container'); const prevBtn = document.getElementById('prev'); const nextBtn = document.getElementById('next'); const currentEl = document.getElementById('current'); const showBtn = document.getElementById('show'); const hideBtn = document.getElementById('hide'); const questionEl = document.getElementById('question'); const answerEl = document.getElementById('answer'); const addCardBtn = document.getElementById('add-card'); const clearBtn = document.getElementById('clear'); const addContainer = document.getElementById('add-container'); // Keep track of current card let currentActiveCard = 0; // Store DOM cards const cardsEl = []; // Store card data const cardsData = getCardsData(); // const cardsData = [ // { // question: 'What must a variable begin with?', // answer: 'A letter, $ or _' // }, // { // question: 'What is a variable?', // answer: 'Container for a piece of data' // }, // { // question: 'Example of Case Sensitive Variable', // answer: 'thisIsAVariable' // } // ]; // Create all cards function createCards() { cardsData.forEach((data, index) => createCard(data, index)); } // Create a single card in DOM function createCard(data, index) { const card = document.createElement('div'); card.classList.add('card'); if (index === 0) { card.classList.add('active'); } card.innerHTML = `

${data.question}

${data.answer}

`; card.addEventListener('click', () => card.classList.toggle('show-answer')); // Add to DOM cards cardsEl.push(card); cardsContainer.appendChild(card); updateCurrentText(); } // Show number of cards function updateCurrentText() { currentEl.innerText = `${currentActiveCard + 1}/${cardsEl.length}`; } // Get cards from local storage function getCardsData() { const cards = JSON.parse(localStorage.getItem('cards')); return cards === null ? [] : cards; } // Add card to local storage function setCardsData(cards) { localStorage.setItem('cards', JSON.stringify(cards)); window.location.reload(); } createCards(); // Event listeners // Next button nextBtn.addEventListener('click', () => { cardsEl[currentActiveCard].className = 'card left'; currentActiveCard = currentActiveCard + 1; if (currentActiveCard > cardsEl.length - 1) { currentActiveCard = cardsEl.length - 1; } cardsEl[currentActiveCard].className = 'card active'; updateCurrentText(); }); // Prev button prevBtn.addEventListener('click', () => { cardsEl[currentActiveCard].className = 'card right'; currentActiveCard = currentActiveCard - 1; if (currentActiveCard < 0) { currentActiveCard = 0; } cardsEl[currentActiveCard].className = 'card active'; updateCurrentText(); }); // Show add container showBtn.addEventListener('click', () => addContainer.classList.add('show')); // Hide add container hideBtn.addEventListener('click', () => addContainer.classList.remove('show')); // Add new card addCardBtn.addEventListener('click', () => { const question = questionEl.value; const answer = answerEl.value; if (question.trim() && answer.trim()) { const newCard = { question, answer }; createCard(newCard); questionEl.value = ''; answerEl.value = ''; addContainer.classList.remove('show'); cardsData.push(newCard); setCardsData(cardsData); } }); // Clear cards button clearBtn.addEventListener('click', () => { localStorage.clear(); cardsContainer.innerHTML = ''; window.location.reload(); }); ================================================ FILE: memory-cards/style.css ================================================ @import url('https://fonts.googleapis.com/css?family=Lato:300,500,700&display=swap'); * { box-sizing: border-box; } body { background-color: #fff; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; overflow: hidden; font-family: 'Lato', sans-serif; } h1 { position: relative; } h1 button { position: absolute; right: 0; transform: translate(120%, -50%); z-index: 2; } .btn { cursor: pointer; background-color: #fff; border: 1px solid #aaa; border-radius: 3px; font-size: 14px; margin-top: 20px; padding: 10px 15px; } .btn-small { font-size: 12px; padding: 5px 10px; } .btn-ghost { border: 0; background-color: transparent; } .clear { position: absolute; bottom: 30px; left: 30px; } .cards { perspective: 1000px; position: relative; height: 300px; width: 500px; max-width: 100%; } .card { position: absolute; opacity: 0; font-size: 1.5em; top: 0; left: 0; height: 100%; width: 100%; transform: translateX(50%) rotateY(-10deg); transition: transform 0.4s ease, opacity 0.4s ease; } .card.active { cursor: pointer; opacity: 1; z-index: 10; transform: translateX(0) rotateY(0deg); } .card.left { transform: translateX(-50%) rotateY(10deg); } .inner-card { box-shadow: 0 1px 10px rgba(0, 0, 0, 0.3); border-radius: 4px; height: 100%; width: 100%; position: relative; transform-style: preserve-3d; transition: transform 0.4s ease; } .card.show-answer .inner-card { transform: rotateX(180deg); } .inner-card-front, .inner-card-back { backface-visibility: hidden; position: absolute; top: 0; left: 0; display: flex; align-items: center; justify-content: center; height: 100%; width: 100%; background: #fff; } .inner-card-front { transform: rotateX(0deg); z-index: 2; } .inner-card-back { transform: rotateX(180deg); } .inner-card-front::after, .inner-card-back::after { content: '\f021 Flip'; font-family: 'Font Awesome 5 Free', Lato, sans-serif; position: absolute; top: 10px; right: 10px; font-weight: bold; font-size: 16px; color: #ddd; } .navigation { display: flex; margin: 20px 0; } .navigation .nav-button { border: none; background-color: transparent; cursor: pointer; font-size: 16px; } .navigation p { margin: 0 25px; } .add-container { opacity: 0; z-index: -1; background-color: #f0f0f0; border-top: 2px solid #eee; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 10px 0; position: absolute; top: 0; bottom: 0; width: 100%; transition: 0.3s ease; } .add-container.show { opacity: 1; z-index: 2; } .add-container h3 { margin: 10px 0; } .form-group label { display: block; margin: 20px 0 10px; } .form-group textarea { border: 1px solid #aaa; border-radius: 3px; font-size: 16px; padding: 12px; min-width: 500px; max-width: 100%; } ================================================ FILE: modal-menu-slider/README.md ================================================ ## Modal & Menu Slider Simple landing page with sliding menu and modal ## Project Specifications - Create and style landing page - Style side nav and modal - Add functionality to make menu open/close on button click - Add functionality to make modal open/close on button click ================================================ FILE: modal-menu-slider/index.html ================================================ My Landing Page

My Landing Page

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Tenetur, amet!

What is this landing page about?

Lorem ipsum dolor sit amet consectetur adipisicing elit. Mollitia iure accusamus ab consectetur eos provident ipsa est perferendis architecto. Provident, quaerat asperiores. Quo esse minus repellat sapiente, impedit obcaecati necessitatibus.

Lorem, ipsum dolor sit amet consectetur adipisicing elit. Sapiente optio officia ipsa. Cum dignissimos possimus et non provident facilis saepe!

Tell Me More

Lorem ipsum dolor sit amet consectetur, adipisicing elit. Repellat eaque delectus explicabo qui reprehenderit? Aspernatur ad similique minima accusamus maiores accusantium libero autem iusto reiciendis ullam impedit esse quibusdam, deleniti laudantium rerum beatae, deserunt nemo neque, obcaecati exercitationem sit. Earum.

Benefits

Lorem ipsum dolor sit amet consectetur, adipisicing elit. Esse quam nostrum, fugiat, itaque natus officia laborum dolorum id accusantium culpa architecto tenetur fuga? Consequatur provident rerum eius ratione dolor officiis doloremque minima optio dignissimos doloribus odio debitis vero cumque itaque excepturi a neque, expedita nulla earum accusamus repellat adipisci veritatis quam. Ipsum fugiat iusto pariatur consectetur quas libero dolor dolores dolorem, nostrum ducimus doloremque placeat accusamus iste, culpa quaerat consequatur?

================================================ FILE: modal-menu-slider/script.js ================================================ const toggle = document.getElementById('toggle'); const close = document.getElementById('close'); const open = document.getElementById('open'); const modal = document.getElementById('modal'); const navbar = document.getElementById('navbar'); // This function closes navbar if user clicks anywhere outside of navbar once it's opened // Does not leave unused event listeners on // It's messy, but it works function closeNavbar(e) { if ( document.body.classList.contains('show-nav') && e.target !== toggle && !toggle.contains(e.target) && e.target !== navbar && !navbar.contains(e.target) ) { document.body.classList.toggle('show-nav'); document.body.removeEventListener('click', closeNavbar); } else if (!document.body.classList.contains('show-nav')) { document.body.removeEventListener('click', closeNavbar); } } // Toggle nav toggle.addEventListener('click', () => { document.body.classList.toggle('show-nav'); document.body.addEventListener('click', closeNavbar); }); // Show modal open.addEventListener('click', () => modal.classList.add('show-modal')); // Hide modal close.addEventListener('click', () => modal.classList.remove('show-modal')); // Hide modal on outside click window.addEventListener('click', e => e.target == modal ? modal.classList.remove('show-modal') : false ); ================================================ FILE: modal-menu-slider/style.css ================================================ @import url('https://fonts.googleapis.com/css?family=Lato&display=swap'); :root { --modal-duration: 1s; --primary-color: #30336b; --secondary-color: #be2edd; } * { box-sizing: border-box; } body { font-family: 'Lato', sans-serif; margin: 0; transition: transform 0.3s ease; } body.show-nav { /* Width of nav */ transform: translateX(200px); } nav { background-color: var(--primary-color); border-right: 2px solid rgba(200, 200, 200, 0.1); color: #fff; position: fixed; top: 0; left: 0; width: 200px; height: 100%; z-index: 100; transform: translateX(-100%); } nav .logo { padding: 30px 0; text-align: center; } nav .logo img { height: 75px; width: 75px; border-radius: 50%; } nav ul { padding: 0; list-style-type: none; margin: 0; } nav ul li { border-bottom: 2px solid rgba(200, 200, 200, 0.1); padding: 20px; } nav ul li:first-of-type { border-top: 2px solid rgba(200, 200, 200, 0.1); } nav ul li a { color: #fff; text-decoration: none; } nav ul li a:hover { text-decoration: underline; } header { background-color: var(--primary-color); color: #fff; font-size: 130%; position: relative; padding: 40px 15px; text-align: center; } header h1 { margin: 0; } header p { margin: 30px 0; } button, input[type='submit'] { background-color: var(--secondary-color); border: 0; border-radius: 5px; color: #fff; cursor: pointer; padding: 8px 12px; } button:focus { outline: none; } .toggle { background-color: rgba(0, 0, 0, 0.3); position: absolute; top: 20px; left: 20px; } .cta-btn { padding: 12px 30px; font-size: 20px; } .container { padding: 15px; margin: 0 auto; max-width: 100%; width: 800px; } .modal-container { background-color: rgba(0, 0, 0, 0.6); display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; } .modal-container.show-modal { display: block; } .modal { background-color: #fff; border-radius: 5px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); position: absolute; overflow: hidden; top: 50%; left: 50%; transform: translate(-50%, -50%); max-width: 100%; width: 400px; animation-name: modalopen; animation-duration: var(--modal-duration); } .modal-header { background: var(--primary-color); color: #fff; padding: 15px; } .modal-header h3 { margin: 0; border-bottom: 1px solid #333; } .modal-content { padding: 20px; } .modal-form div { margin: 15px 0; } .modal-form label { display: block; margin-bottom: 5px; } .modal-form .form-input { padding: 8px; width: 100%; } .close-btn { background: transparent; font-size: 25px; position: absolute; top: 0; right: 0; } @keyframes modalopen { from { opacity: 0; } to { opacity: 1; } } ================================================ FILE: movie-seat-booking/README.md ================================================ ## Movie Seat Booking Display movie choices and seats in a theater to select from in order to purchase tickets ## Project Specifications - Display UI with movie select, screen, seats, legend & seat info - User can select a movie/price - User can select/deselect seats - User can not select occupied seats - Number of seats and price will update - Save seats, movie and price to local storage so that UI is still populated on refresh Design inspiration from [Dribbble](https://dribbble.com/shots/3628370-Movie-Seat-Booking) ================================================ FILE: movie-seat-booking/index.html ================================================ Movie Seat Booking

You have selected 0 seats for a price of $0

================================================ FILE: movie-seat-booking/script.js ================================================ const container = document.querySelector('.container'); const seats = document.querySelectorAll('.row .seat:not(.occupied)'); const count = document.getElementById('count'); const total = document.getElementById('total'); const movieSelect = document.getElementById('movie'); populateUI(); let ticketPrice = +movieSelect.value; // Save selected movie index and price function setMovieData(movieIndex, moviePrice) { localStorage.setItem('selectedMovieIndex', movieIndex); localStorage.setItem('selectedMoviePrice', moviePrice); } // Update total and count function updateSelectedCount() { const selectedSeats = document.querySelectorAll('.row .seat.selected'); const seatsIndex = [...selectedSeats].map(seat => [...seats].indexOf(seat)); localStorage.setItem('selectedSeats', JSON.stringify(seatsIndex)); const selectedSeatsCount = selectedSeats.length; count.innerText = selectedSeatsCount; total.innerText = selectedSeatsCount * ticketPrice; setMovieData(movieSelect.selectedIndex, movieSelect.value); } // Get data from localstorage and populate UI function populateUI() { const selectedSeats = JSON.parse(localStorage.getItem('selectedSeats')); if (selectedSeats !== null && selectedSeats.length > 0) { seats.forEach((seat, index) => { if (selectedSeats.indexOf(index) > -1) { seat.classList.add('selected'); } }); } const selectedMovieIndex = localStorage.getItem('selectedMovieIndex'); if (selectedMovieIndex !== null) { movieSelect.selectedIndex = selectedMovieIndex; } } // Movie select event movieSelect.addEventListener('change', e => { ticketPrice = +e.target.value; setMovieData(e.target.selectedIndex, e.target.value); updateSelectedCount(); }); // Seat click event container.addEventListener('click', e => { if ( e.target.classList.contains('seat') && !e.target.classList.contains('occupied') ) { e.target.classList.toggle('selected'); updateSelectedCount(); } }); // Initial count and total set updateSelectedCount(); ================================================ FILE: movie-seat-booking/style.css ================================================ @import url('https://fonts.googleapis.com/css?family=Lato&display=swap'); * { box-sizing: border-box; } body { background-color: #242333; color: #fff; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; font-family: 'Lato', sans-serif; margin: 0; } .movie-container { margin: 20px 0; } .movie-container select { background-color: #fff; border: 0; border-radius: 5px; font-size: 14px; margin-left: 10px; padding: 5px 15px 5px 15px; -moz-appearance: none; -webkit-appearance: none; appearance: none; } .container { perspective: 1000px; margin-bottom: 30px; } .seat { background-color: #444451; height: 12px; width: 15px; margin: 3px; border-top-left-radius: 10px; border-top-right-radius: 10px; } .seat.selected { background-color: #6feaf6; } .seat.occupied { background-color: #fff; } .seat:nth-of-type(2) { margin-right: 18px; } .seat:nth-last-of-type(2) { margin-left: 18px; } .seat:not(.occupied):hover { cursor: pointer; transform: scale(1.2); } .showcase .seat:not(.occupied):hover { cursor: default; transform: scale(1); } .showcase { background: rgba(0, 0, 0, 0.1); padding: 5px 10px; border-radius: 5px; color: #777; list-style-type: none; display: flex; justify-content: space-between; } .showcase li { display: flex; align-items: center; justify-content: center; margin: 0 10px; } .showcase li small { margin-left: 2px; } .row { display: flex; } .screen { background-color: #fff; height: 70px; width: 100%; margin: 15px 0; transform: rotateX(-45deg); box-shadow: 0 3px 10px rgba(255, 255, 255, 0.7); } p.text { margin: 5px 0; } p.text span { color: #6feaf6; } ================================================ FILE: music-player/index.html ================================================ Music Player

Music Player

music-cover
================================================ FILE: music-player/readme.md ================================================ ## Music Player Create beautiful UI to play music stored in the "music folder" using the HTML5 audio API ## Project Specifications - Create UI for music player including spinning image and song detail popup - Add play and pause functionality - Switch songs - Progress bar ================================================ FILE: music-player/script.js ================================================ const musicContainer = document.getElementById('music-container'); const playBtn = document.getElementById('play'); const prevBtn = document.getElementById('prev'); const nextBtn = document.getElementById('next'); const audio = document.getElementById('audio'); const progress = document.getElementById('progress'); const progressContainer = document.getElementById('progress-container'); const title = document.getElementById('title'); const cover = document.getElementById('cover'); const currTime = document.querySelector('#currTime'); const durTime = document.querySelector('#durTime'); // Song titles const songs = ['hey', 'summer', 'ukulele']; // Keep track of song let songIndex = 2; // Initially load song details into DOM loadSong(songs[songIndex]); // Update song details function loadSong(song) { title.innerText = song; audio.src = `music/${song}.mp3`; cover.src = `images/${song}.jpg`; } // Play song function playSong() { musicContainer.classList.add('play'); playBtn.querySelector('i.fas').classList.remove('fa-play'); playBtn.querySelector('i.fas').classList.add('fa-pause'); audio.play(); } // Pause song function pauseSong() { musicContainer.classList.remove('play'); playBtn.querySelector('i.fas').classList.add('fa-play'); playBtn.querySelector('i.fas').classList.remove('fa-pause'); audio.pause(); } // Previous song function prevSong() { songIndex--; if (songIndex < 0) { songIndex = songs.length - 1; } loadSong(songs[songIndex]); playSong(); } // Next song function nextSong() { songIndex++; if (songIndex > songs.length - 1) { songIndex = 0; } loadSong(songs[songIndex]); playSong(); } // Update progress bar function updateProgress(e) { const { duration, currentTime } = e.srcElement; const progressPercent = (currentTime / duration) * 100; progress.style.width = `${progressPercent}%`; } // Set progress bar function setProgress(e) { const width = this.clientWidth; const clickX = e.offsetX; const duration = audio.duration; audio.currentTime = (clickX / width) * duration; } //get duration & currentTime for Time of song function DurTime (e) { const {duration,currentTime} = e.srcElement; var sec; var sec_d; // define minutes currentTime let min = (currentTime==null)? 0: Math.floor(currentTime/60); min = min <10 ? '0'+min:min; // define seconds currentTime function get_sec (x) { if(Math.floor(x) >= 60){ for (var i = 1; i<=60; i++){ if(Math.floor(x)>=(60*i) && Math.floor(x)<(60*(i+1))) { sec = Math.floor(x) - (60*i); sec = sec <10 ? '0'+sec:sec; } } }else{ sec = Math.floor(x); sec = sec <10 ? '0'+sec:sec; } } get_sec (currentTime,sec); // change currentTime DOM currTime.innerHTML = min +':'+ sec; // define minutes duration let min_d = (isNaN(duration) === true)? '0': Math.floor(duration/60); min_d = min_d <10 ? '0'+min_d:min_d; function get_sec_d (x) { if(Math.floor(x) >= 60){ for (var i = 1; i<=60; i++){ if(Math.floor(x)>=(60*i) && Math.floor(x)<(60*(i+1))) { sec_d = Math.floor(x) - (60*i); sec_d = sec_d <10 ? '0'+sec_d:sec_d; } } }else{ sec_d = (isNaN(duration) === true)? '0': Math.floor(x); sec_d = sec_d <10 ? '0'+sec_d:sec_d; } } // define seconds duration get_sec_d (duration); // change duration DOM durTime.innerHTML = min_d +':'+ sec_d; }; // Event listeners playBtn.addEventListener('click', () => { const isPlaying = musicContainer.classList.contains('play'); if (isPlaying) { pauseSong(); } else { playSong(); } }); // Change song prevBtn.addEventListener('click', prevSong); nextBtn.addEventListener('click', nextSong); // Time/song update audio.addEventListener('timeupdate', updateProgress); // Click on progress bar progressContainer.addEventListener('click', setProgress); // Song ends audio.addEventListener('ended', nextSong); // Time of song audio.addEventListener('timeupdate',DurTime); ================================================ FILE: music-player/style.css ================================================ @import url('https://fonts.googleapis.com/css?family=Lato&display=swap'); * { box-sizing: border-box; } body { background-image: linear-gradient( 0deg, rgba(247, 247, 247, 1) 23.8%, rgba(252, 221, 221, 1) 92% ); height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-family: 'Lato', sans-serif; margin: 0; } .music-container { background-color: #fff; border-radius: 15px; box-shadow: 0 20px 20px 0 rgba(252, 169, 169, 0.6); display: flex; padding: 20px 30px; position: relative; margin: 100px 0; z-index: 10; } .img-container { position: relative; width: 110px; } .img-container::after { content: ''; background-color: #fff; border-radius: 50%; position: absolute; bottom: 100%; left: 50%; width: 20px; height: 20px; transform: translate(-50%, 50%); } .img-container img { border-radius: 50%; object-fit: cover; height: 110px; width: inherit; position: absolute; bottom: 0; left: 0; animation: rotate 3s linear infinite; animation-play-state: paused; } .music-container.play .img-container img { animation-play-state: running; } @keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .navigation { display: flex; align-items: center; justify-content: center; z-index: 1; } .action-btn { background-color: #fff; border: 0; color: #dfdbdf; font-size: 20px; cursor: pointer; padding: 10px; margin: 0 20px; } .action-btn.action-btn-big { color: #cdc2d0; font-size: 30px; } .action-btn:focus { outline: 0; } .music-info { background-color: rgba(255, 255, 255, 0.5); border-radius: 15px 15px 0 0; position: absolute; top: 0; left: 20px; width: calc(100% - 40px); padding: 10px 10px 10px 150px; opacity: 0; transform: translateY(0%); transition: transform 0.3s ease-in, opacity 0.3s ease-in; z-index: 0; } .music-container.play .music-info { opacity: 1; transform: translateY(-100%); } .music-info h4 { margin: 0; } .progress-container { background: #fff; border-radius: 5px; cursor: pointer; margin: 10px 0; height: 4px; width: 100%; } .progress { background-color: #fe8daa; border-radius: 5px; height: 100%; width: 0%; transition: width 0.1s linear; } ================================================ FILE: new-year-countdown/README.md ================================================ ## New Year Countdown Landing page that counts down from the current date to the next new year ## Project Specifications - Create landing page with HTML/CSS - Calculate the days, hours, mins and seconds to the new year - Insert values into the DOM - Show a spinner right before loading the countdown - Show the coming year in the background ================================================ FILE: new-year-countdown/index.html ================================================ New Year Countdown

New Year Countdown

00

days

00

hours

00

minutes

00

seconds
Loading... ================================================ FILE: new-year-countdown/script.js ================================================ const days = document.getElementById('days'); const hours = document.getElementById('hours'); const minutes = document.getElementById('minutes'); const seconds = document.getElementById('seconds'); const countdown = document.getElementById('countdown'); const year = document.getElementById('year'); const loading = document.getElementById('loading'); const currentYear = new Date().getFullYear(); const newYearTime = new Date(`January 01 ${currentYear + 1} 00:00:00`); // Set background year year.innerText = currentYear + 1; // Update countdown time function updateCountdown() { const currentTime = new Date(); const diff = newYearTime - currentTime; const d = Math.floor(diff / 1000 / 60 / 60 / 24); const h = Math.floor(diff / 1000 / 60 / 60) % 24; const m = Math.floor(diff / 1000 / 60) % 60; const s = Math.floor(diff / 1000) % 60; // Add values to DOM days.innerHTML = d; hours.innerHTML = h < 10 ? '0' + h : h; minutes.innerHTML = m < 10 ? '0' + m : m; seconds.innerHTML = s < 10 ? '0' + s : s; } // Show spinner before countdown setTimeout(() => { loading.remove(); countdown.style.display = 'flex'; }, 1000); // Run every second setInterval(updateCountdown, 1000); ================================================ FILE: new-year-countdown/style.css ================================================ @import url('https://fonts.googleapis.com/css?family=Lato&display=swap'); * { box-sizing: border-box; } body { background-image: url('https://images.unsplash.com/photo-1467810563316-b5476525c0f9?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1349&q=80'); background-repeat: no-repeat; background-size: cover; background-position: center center; height: 100vh; color: #fff; font-family: 'Lato', sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; margin: 0; overflow: hidden; } /* Add a dark overlay */ body::after { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); } body * { z-index: 1; } h1 { font-size: 60px; margin: -80px 0 40px; } .year { font-size: 200px; z-index: -1; opacity: 0.2; position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); } .countdown { /* display: flex; */ display: none; transform: scale(2); } .time { display: flex; flex-direction: column; align-items: center; justify-content: center; margin: 15px; } .time h2 { margin: 0 0 5px; } @media (max-width: 500px) { h1 { font-size: 45px; } .time { margin: 5px; } .time h2 { font-size: 12px; margin: 0; } .time small { font-size: 10px; } } ================================================ FILE: product-filtering/index.html ================================================ Product Filtering

Filters

Category

================================================ FILE: product-filtering/script.js ================================================ const products = [ { name: 'Sony Playstation 5', url: 'images/playstation_5.png', type: 'games', price: 499.99, }, { name: 'Samsung Galaxy', url: 'images/samsung_galaxy.png', type: 'smartphones', price: 399.99, }, { name: 'Cannon EOS Camera', url: 'images/cannon_eos_camera.png', type: 'cameras', price: 749.99, }, { name: 'Sony A7 Camera', url: 'images/sony_a7_camera.png', type: 'cameras', price: 1999.99, }, { name: 'LG TV', url: 'images/lg_tv.png', type: 'televisions', price: 799.99, }, { name: 'Nintendo Switch', url: 'images/nintendo_switch.png', type: 'games', price: 299.99, }, { name: 'Xbox Series X', url: 'images/xbox_series_x.png', type: 'games', price: 499.99, }, { name: 'Samsung TV', url: 'images/samsung_tv.png', type: 'televisions', price: 1099.99, }, { name: 'Google Pixel', url: 'images/google_pixel.png', type: 'smartphones', price: 499.99, }, { name: 'Sony ZV1F Camera', url: 'images/sony_zv1f_camera.png', type: 'cameras', price: 799.99, }, { name: 'Toshiba TV', url: 'images/toshiba_tv.png', type: 'televisions', price: 499.99, }, { name: 'iPhone 14', url: 'images/iphone_14.png', type: 'smartphones', price: 999.99, }, ]; // Get DOM elements const productsWrapperEl = document.getElementById('products-wrapper'); const checkEls = document.querySelectorAll('.check'); const filtersContainer = document.getElementById('filters-container'); const searchInput = document.getElementById('search'); const cartButton = document.getElementById('cartButton'); const cartCount = document.getElementById('cartCount'); // Initialize cart item count let cartItemCount = 0; // Initialize products const productsEls = []; // Loop over the products and create the product elements products.forEach((product) => { const productEl = createProductElement(product); productsEls.push(productEl); productsWrapperEl.appendChild(productEl); }); // Add filter event listeners filtersContainer.addEventListener('change', filterProducts); searchInput.addEventListener('input', filterProducts); // Create product element function createProductElement(product) { const productEl = document.createElement('div'); productEl.className = 'item space-y-2'; productEl.innerHTML = `
${product.name} Add To Cart

${product.name}

$${product.price.toLocaleString()}`; productEl.querySelector('.status').addEventListener('click', addToCart); return productEl; } // Toggle add/remove from cart function addToCart(e) { const statusEl = e.target; if (statusEl.classList.contains('added')) { // Remove from cart statusEl.classList.remove('added'); statusEl.innerText = 'Add To Cart'; statusEl.classList.remove('bg-red-600'); statusEl.classList.add('bg-gray-800'); cartItemCount--; } else { // Add to cart statusEl.classList.add('added'); statusEl.innerText = 'Remove From Cart'; statusEl.classList.remove('bg-gray-800'); statusEl.classList.add('bg-red-600'); cartItemCount++; } // Update cart item count cartCount.innerText = cartItemCount.toString(); } // Filter products by search or checkbox function filterProducts() { // Get search term const searchTerm = searchInput.value.trim().toLowerCase(); // Get checked categories const checkedCategories = Array.from(checkEls) .filter((check) => check.checked) .map((check) => check.id); // Loop over products and check for matches productsEls.forEach((productEl, index) => { const product = products[index]; // Check to see if product matches the search or checked items const matchesSearchTerm = product.name.toLowerCase().includes(searchTerm); const isInCheckedCategory = checkedCategories.length === 0 || checkedCategories.includes(product.type); // Show or hide product based on matches if (matchesSearchTerm && isInCheckedCategory) { productEl.classList.remove('hidden'); } else { productEl.classList.add('hidden'); } }); } ================================================ FILE: readme.md ================================================ # 20+ Web Projects With Vanilla JavaScript This is the main repository for all of the projects in the course. - [Course Link](https://www.traversymedia.com/20-Vanilla-JavaScript-Projects) - [Get Course On Udemy](https://www.udemy.com/course/web-projects-with-vanilla-javascript/?referralCode=F9B7C7FED834F91ADE75) | # | Project | Live Demo | | :-: | :----------------------------: | :-------: | | 01 | [Form Validator](https://github.com/bradtraversy/vanillawebprojects/tree/master/form-validator) | [Live Demo](https://vanillawebprojects.com/projects/form-validator/) | | 02 | [Movie Seat Booking](https://github.com/bradtraversy/vanillawebprojects/tree/master/movie-seat-booking) | [Live Demo](https://vanillawebprojects.com/projects/movie-seat-booking/) | | 03 | [Custom Video Player](https://github.com/bradtraversy/vanillawebprojects/tree/master/custom-video-player) | [Live Demo](https://vanillawebprojects.com/projects/custom-video-player/) | | 04 | [Exchange Rate Calculator](https://github.com/bradtraversy/vanillawebprojects/tree/master/exchange-rate) | [Live Demo](https://vanillawebprojects.com/projects/exchange-rate/) | | 05 | [DOM Array Methods Project](https://github.com/bradtraversy/vanillawebprojects/tree/master/dom-array-methods) | [Live Demo](https://vanillawebprojects.com/projects/dom-array-methods/) | | 06 | [Menu Slider & Modal](https://github.com/bradtraversy/vanillawebprojects/tree/master/modal-menu-slider) | [Live Demo](https://vanillawebprojects.com/projects/modal-menu-slider/) | | 07 | [Hangman Game](https://github.com/bradtraversy/vanillawebprojects/tree/master/hangman) | [Live Demo](https://vanillawebprojects.com/projects/hangman/) | | 08 | [Mealfinder App](https://github.com/bradtraversy/vanillawebprojects/tree/master/meal-finder) | [Live Demo](https://vanillawebprojects.com/projects/meal-finder/) | | 09 | [Expense Tracker](https://github.com/bradtraversy/vanillawebprojects/tree/master/expense-tracker) | [Live Demo](https://vanillawebprojects.com/projects/expense-tracker/) | | 10 | [Music Player](https://github.com/bradtraversy/vanillawebprojects/tree/master/music-player) | [Live Demo](https://vanillawebprojects.com/projects/music-player/) | | 11 | [Infinite Scrolling](https://github.com/bradtraversy/vanillawebprojects/tree/master/infinite_scroll_blog) | [Live Demo](https://vanillawebprojects.com/projects/infinite_scroll_blog/) | | 12 | [Typing Game](https://github.com/bradtraversy/vanillawebprojects/tree/master/typing-game) | [Live Demo](https://vanillawebprojects.com/projects/typing-game/) | | 13 | [Speech Text Reader](https://github.com/bradtraversy/vanillawebprojects/tree/master/speech-text-reader) | [Live Demo](https://vanillawebprojects.com/projects/speech-text-reader/) | | 14 | [Memory Cards](https://github.com/bradtraversy/vanillawebprojects/tree/master/memory-cards) | [Live Demo](https://vanillawebprojects.com/projects/memory-cards/) | | 15 | [LyricsSearch App](https://github.com/bradtraversy/vanillawebprojects/tree/master/lyrics-search) | [Live Demo](https://vanillawebprojects.com/projects/lyrics-search/) | | 16 | [Relaxer App](https://github.com/bradtraversy/vanillawebprojects/tree/master/relaxer-app) | [Live Demo](https://vanillawebprojects.com/projects//relaxer-app/) | | 17 | [Breakout Game](https://github.com/bradtraversy/vanillawebprojects/tree/master/breakout-game) | [Live Demo](https://vanillawebprojects.com/projects/breakout-game/) | | 18 | [New Year Countdown](https://github.com/bradtraversy/vanillawebprojects/tree/master/new-year-countdown) | [Live Demo](https://vanillawebprojects.com/projects/new-year-countdown/) | | 19 | [Speak Number Guessing Game](https://github.com/bradtraversy/vanillawebprojects/tree/master/speak-number-guess) | [Live Demo](https://vanillawebprojects.com/projects/speak-number-guess/) | | 20 | [Product Filtering UI](https://github.com/bradtraversy/vanillawebprojects/tree/master/product-filtering) | [Live Demo](https://vanillawebprojects.com/projects/product-filtering/) | NOTE ON PULL REQUESTS: All of these projects are part of the course. While I do appreciate people trying to make some things prettier or adding new features, we are only accepting pull requests and looking at issues for bug fixes so that the code stays inline with the course ================================================ FILE: relaxer-app/README.md ================================================ ## Relaxer App A relaxing breathing app with a visual director to tell you when to breathe in, hold and breathe out ## Project Specifications - Create circle and gradient circle with CSS - Create and animate pointer (Small circle) - Create grow and shrink animations - Add JavaScript to create the breath animation effect ================================================ FILE: relaxer-app/index.html ================================================ Relaxer

Relaxer

================================================ FILE: relaxer-app/script.js ================================================ const container = document.getElementById('container'); const text = document.getElementById('text'); const totalTime = 7500; const breatheTime = (totalTime / 5) * 2; const holdTime = totalTime / 5; breathAnimation(); function breathAnimation() { text.innerText = 'Breathe In!'; container.className = 'container grow'; setTimeout(() => { text.innerText = 'Hold'; setTimeout(() => { text.innerText = 'Breathe Out!'; container.className = 'container shrink'; }, holdTime); }, breatheTime); } setInterval(breathAnimation, totalTime); ================================================ FILE: relaxer-app/style.css ================================================ @import url('https://fonts.googleapis.com/css?family=Montserrat&display=swap'); * { box-sizing: border-box; } body { background: #224941 url('./img/bg.jpg') no-repeat center center/cover; color: #fff; font-family: 'Montserrat', sans-serif; min-height: 100vh; overflow: hidden; display: flex; flex-direction: column; align-items: center; margin: 0; } .container { display: flex; align-items: center; justify-content: center; margin: auto; height: 300px; width: 300px; position: relative; transform: scale(1); } .circle { background-color: #010f1c; height: 100%; width: 100%; border-radius: 50%; position: absolute; top: 0; left: 0; z-index: -1; } .gradient-circle { background: conic-gradient( #55b7a4 0%, #4ca493 40%, #fff 40%, #fff 60%, #336d62 60%, #2a5b52 100% ); height: 320px; width: 320px; z-index: -2; border-radius: 50%; position: absolute; top: -10px; left: -10px; } .pointer { background-color: #fff; border-radius: 50%; height: 20px; width: 20px; display: block; } .pointer-container { position: absolute; top: -40px; left: 140px; width: 20px; height: 190px; animation: rotate 7.5s linear forwards infinite; transform-origin: bottom center; } @keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .container.grow { animation: grow 3s linear forwards; } @keyframes grow { from { transform: scale(1); } to { transform: scale(1.2); } } .container.shrink { animation: shrink 3s linear forwards; } @keyframes shrink { from { transform: scale(1.2); } to { transform: scale(1); } } ================================================ FILE: sortable-list/README.md ================================================ ## Sortable List Display a scrambled list that can be sorted with drag and drop ## Project Specifications - Create an ordered list (Top 10 richest people) - Scramble list items randomly - Allow user to drag and drop an item to a different position - Button to check if items are in correct order - Show green for correct order and red for wrong order ================================================ FILE: sortable-list/index.html ================================================ Top 10 Richest People

10 Richest People

Drag and drop the items into their corresponding spots

================================================ FILE: sortable-list/script.js ================================================ const draggable_list = document.getElementById('draggable-list'); const check = document.getElementById('check'); const richestPeople = [ 'Jeff Bezos', 'Bill Gates', 'Warren Buffett', 'Bernard Arnault', 'Carlos Slim Helu', 'Amancio Ortega', 'Larry Ellison', 'Mark Zuckerberg', 'Michael Bloomberg', 'Larry Page' ]; // Store listitems const listItems = []; let dragStartIndex; createList(); // Insert list items into DOM function createList() { [...richestPeople] .map(a => ({ value: a, sort: Math.random() })) .sort((a, b) => a.sort - b.sort) .map(a => a.value) .forEach((person, index) => { const listItem = document.createElement('li'); listItem.setAttribute('data-index', index); listItem.innerHTML = ` ${index + 1}

${person}

`; listItems.push(listItem); draggable_list.appendChild(listItem); }); addEventListeners(); } function dragStart() { // console.log('Event: ', 'dragstart'); dragStartIndex = +this.closest('li').getAttribute('data-index'); } function dragEnter() { // console.log('Event: ', 'dragenter'); this.classList.add('over'); } function dragLeave() { // console.log('Event: ', 'dragleave'); this.classList.remove('over'); } function dragOver(e) { // console.log('Event: ', 'dragover'); e.preventDefault(); } function dragDrop() { // console.log('Event: ', 'drop'); const dragEndIndex = +this.getAttribute('data-index'); swapItems(dragStartIndex, dragEndIndex); this.classList.remove('over'); } // Swap list items that are drag and drop function swapItems(fromIndex, toIndex) { const itemOne = listItems[fromIndex].querySelector('.draggable'); const itemTwo = listItems[toIndex].querySelector('.draggable'); listItems[fromIndex].appendChild(itemTwo); listItems[toIndex].appendChild(itemOne); } // Check the order of list items function checkOrder() { listItems.forEach((listItem, index) => { const personName = listItem.querySelector('.draggable').innerText.trim(); if (personName !== richestPeople[index]) { listItem.classList.add('wrong'); } else { listItem.classList.remove('wrong'); listItem.classList.add('right'); } }); } function addEventListeners() { const draggables = document.querySelectorAll('.draggable'); const dragListItems = document.querySelectorAll('.draggable-list li'); draggables.forEach(draggable => { draggable.addEventListener('dragstart', dragStart); }); dragListItems.forEach(item => { item.addEventListener('dragover', dragOver); item.addEventListener('drop', dragDrop); item.addEventListener('dragenter', dragEnter); item.addEventListener('dragleave', dragLeave); }); } check.addEventListener('click', checkOrder); ================================================ FILE: sortable-list/style.css ================================================ @import url('https://fonts.googleapis.com/css?family=Lato&display=swap'); :root { --border-color: #e3e5e4; --background-color: #c3c7ca; --text-color: #34444f; } * { box-sizing: border-box; } body { background-color: #fff; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; height: 100vh; margin: 0; font-family: 'Lato', sans-serif; } .draggable-list { border: 1px solid var(--border-color); color: var(--text-color); padding: 0; list-style-type: none; } .draggable-list li { background-color: #fff; display: flex; flex: 1; } .draggable-list li:not(:last-of-type) { border-bottom: 1px solid var(--border-color); } .draggable-list .number { background-color: var(--background-color); display: flex; align-items: center; justify-content: center; font-size: 28px; height: 60px; width: 60px; } .draggable-list li.over .draggable { background-color: #eaeaea; } .draggable-list .person-name { margin: 0 20px 0 0; } .draggable-list li.right .person-name { color: #3ae374; } .draggable-list li.wrong .person-name { color: #ff3838; } .draggable { cursor: pointer; display: flex; align-items: center; justify-content: space-between; padding: 15px; flex: 1; } .check-btn { background-color: var(--background-color); border: none; color: var(--text-color); font-size: 16px; padding: 10px 20px; cursor: pointer; } .check-btn:active { transform: scale(0.98); } .check-btn:focus { outline: none; } ================================================ FILE: speak-number-guess/index.html ================================================ Speak Number Guess Speak

Guess a Number Between 1 - 100

Speak the number into your microphone

================================================ FILE: speak-number-guess/readme.md ================================================ ## Speak Number Guessing Game Number guessing game where you speak your guess into the microphone using the speech recognition API ## Project Specifications - Display UI directing user to speak guess - Implement speech recognition to listen to mic - Process user's guess and match - Let user know higher, lower, match or not a number ================================================ FILE: speak-number-guess/script.js ================================================ const msgEl = document.getElementById('msg'); const randomNum = getRandomNumber(); console.log('Number:', randomNum); window.SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; let recognition = new window.SpeechRecognition(); // Start recognition and game recognition.start(); // Capture user speak function onSpeak(e) { const msg = e.results[0][0].transcript; writeMessage(msg); checkNumber(msg); } // Write what user speaks function writeMessage(msg) { msgEl.innerHTML = `
You said:
${msg} `; } // Check msg against number function checkNumber(msg) { const num = +msg; // Check if valid number if (Number.isNaN(num)) { msgEl.innerHTML += '
That is not a valid number
'; return; } // Check in range if (num > 100 || num < 1) { msgEl.innerHTML += '
Number must be between 1 and 100
'; return; } // Check number if (num === randomNum) { document.body.innerHTML = `

Congrats! You have guessed the number!

It was ${num}

`; } else if (num > randomNum) { msgEl.innerHTML += '
GO LOWER
'; } else { msgEl.innerHTML += '
GO HIGHER
'; } } // Generate random number function getRandomNumber() { return Math.floor(Math.random() * 100) + 1; } // Speak result recognition.addEventListener('result', onSpeak); // End SR service recognition.addEventListener('end', () => recognition.start()); document.body.addEventListener('click', e => { if (e.target.id == 'play-again') { window.location.reload(); } }); ================================================ FILE: speak-number-guess/style.css ================================================ * { box-sizing: border-box; } body { background: #2f3542 url('img/bg.jpg') no-repeat left center/cover; color: #fff; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; margin: 0; font-family: Arial, Helvetica, sans-serif; } h1, h3 { margin-bottom: 0; } p { line-height: 1.5; margin: 0; } .play-again { padding: 8px 15px; border: 0; background: #f4f4f4; border-radius: 5px; margin-top: 10px; } .msg { font-size: 1.5em; margin-top: 40px; } .box { border: 1px solid #dedede; display: inline-block; font-size: 30px; margin: 20px; padding: 10px; } ================================================ FILE: speech-text-reader/README.md ================================================ ## Speech Text Reader A text to speech app for non-verbal people. Pre-made buttons and custom text speech. This project uses the Web Speech API ## Project Specifications - Create responsive UI (CSS Grid) with picture buttons - Speaks the text when button clicked - Drop down custom text to speech - Speaks the text typed in - Change voices and accents ================================================ FILE: speech-text-reader/index.html ================================================ Speech Text Reader

Speech Text Reader

X

Choose Voice

================================================ FILE: speech-text-reader/script.js ================================================ const main = document.querySelector('main'); const voicesSelect = document.getElementById('voices'); const textarea = document.getElementById('text'); const readBtn = document.getElementById('read'); const toggleBtn = document.getElementById('toggle'); const closeBtn = document.getElementById('close'); const data = [ { image: './img/drink.jpg', text: "I'm Thirsty" }, { image: './img/food.jpg', text: "I'm Hungry" }, { image: './img/tired.jpg', text: "I'm Tired" }, { image: './img/hurt.jpg', text: "I'm Hurt" }, { image: './img/happy.jpg', text: "I'm Happy" }, { image: './img/angry.jpg', text: "I'm Angry" }, { image: './img/sad.jpg', text: "I'm Sad" }, { image: './img/scared.jpg', text: "I'm Scared" }, { image: './img/outside.jpg', text: 'I Want To Go Outside' }, { image: './img/home.jpg', text: 'I Want To Go Home' }, { image: './img/school.jpg', text: 'I Want To Go To School' }, { image: './img/grandma.jpg', text: 'I Want To Go To Grandmas' } ]; data.forEach(createBox); // Create speech boxes function createBox(item) { const box = document.createElement('div'); const { image, text } = item; box.classList.add('box'); box.innerHTML = ` ${text}

${text}

`; box.addEventListener('click', () => { setTextMessage(text); speakText(); // Add active effect box.classList.add('active'); setTimeout(() => box.classList.remove('active'), 800); }); main.appendChild(box); } // Init speech synth const message = new SpeechSynthesisUtterance(); // Store voices let voices = []; function getVoices() { voices = speechSynthesis.getVoices(); voices.forEach(voice => { const option = document.createElement('option'); option.value = voice.name; option.innerText = `${voice.name} ${voice.lang}`; voicesSelect.appendChild(option); }); } // Set text function setTextMessage(text) { message.text = text; } // Speak text function speakText() { speechSynthesis.speak(message); } // Set voice function setVoice(e) { message.voice = voices.find(voice => voice.name === e.target.value); } // Voices changed speechSynthesis.addEventListener('voiceschanged', getVoices); // Toggle text box toggleBtn.addEventListener('click', () => document.getElementById('text-box').classList.toggle('show') ); // Close button closeBtn.addEventListener('click', () => document.getElementById('text-box').classList.remove('show') ); // Change voice voicesSelect.addEventListener('change', setVoice); // Read text button readBtn.addEventListener('click', () => { setTextMessage(textarea.value); speakText(); }); getVoices(); ================================================ FILE: speech-text-reader/style.css ================================================ @import url('https://fonts.googleapis.com/css?family=Lato'); * { box-sizing: border-box; } body { background: #ffefea; font-family: 'Lato', sans-serif; min-height: 100vh; margin: 0; } h1 { text-align: center; } .container { margin: auto; padding: 20px; } .btn { cursor: pointer; background-color: darksalmon; border: 0; border-radius: 4px; color: #fff; font-size: 16px; padding: 8px; } .btn:active { transform: scale(0.98); } .btn:focus, select:focus { outline: 0; } .btn-toggle { display: block; margin: auto; margin-bottom: 20px; } .text-box { width: 70%; position: absolute; top: 30%; left: 50%; transform: translate(-50%, -800px); background-color: #333; color: #fff; padding: 20px; border-radius: 5px; transition: all 1s ease-in-out; } .text-box.show { transform: translate(-50%, 0); } .text-box select { background-color: darksalmon; border: 0; color: #fff; font-size: 12px; height: 30px; width: 100%; } .text-box textarea { border: 1px #dadada solid; border-radius: 4px; font-size: 16px; padding: 8px; margin: 15px 0; width: 100%; height: 150px; } .text-box .btn { width: 100%; } .text-box .close { float: right; text-align: right; cursor: pointer; } main { display: grid; grid-template-columns: repeat(4, 1fr); grid-gap: 10px; } .box { box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); border-radius: 5px; cursor: pointer; display: flex; flex-direction: column; overflow: hidden; transition: box-shadow 0.2s ease-out; } .box.active { box-shadow: 0 0 10px 5px darksalmon; } .box img { width: 100%; object-fit: cover; height: 200px; } .box .info { background-color: darksalmon; color: #fff; font-size: 18px; letter-spacing: 1px; text-transform: uppercase; margin: 0; padding: 10px; text-align: center; height: 100%; } @media (max-width: 1100px) { main { grid-template-columns: repeat(3, 1fr); } } @media (max-width: 760px) { main { grid-template-columns: repeat(2, 1fr); } } @media (max-width: 500px) { main { grid-template-columns: 1fr; } } ================================================ FILE: typing-game/index.html ================================================ Speed Typer

👩‍💻 Speed Typer 👨‍💻

Type the following:

Time left: 10s

Score: 0

================================================ FILE: typing-game/readme.md ================================================ ## Speed Typer Typing Game Game to beat the clock by typing random words ## Project Specifications - Create game UI including a difficuly setting - Generate random word and place in DOM - Score increase after word is typed - Implement timer - Add certain amount of time after word is typed based on difficulty - Store difficulty setting in local storage ================================================ FILE: typing-game/script.js ================================================ const word = document.getElementById('word'); const text = document.getElementById('text'); const scoreEl = document.getElementById('score'); const timeEl = document.getElementById('time'); const endgameEl = document.getElementById('end-game-container'); const settingsBtn = document.getElementById('settings-btn'); const settings = document.getElementById('settings'); const settingsForm = document.getElementById('settings-form'); const difficultySelect = document.getElementById('difficulty'); // List of words for game const words = [ 'sigh', 'tense', 'airplane', 'ball', 'pies', 'juice', 'warlike', 'bad', 'north', 'dependent', 'steer', 'silver', 'highfalutin', 'superficial', 'quince', 'eight', 'feeble', 'admit', 'drag', 'loving' ]; // Init word let randomWord; // Init score let score = 0; // Init time let time = 10; // Set difficulty to value in ls or medium let difficulty = localStorage.getItem('difficulty') !== null ? localStorage.getItem('difficulty') : 'medium'; // Set difficulty select value difficultySelect.value = localStorage.getItem('difficulty') !== null ? localStorage.getItem('difficulty') : 'medium'; // Focus on text on start text.focus(); // Start counting down const timeInterval = setInterval(updateTime, 1000); // Generate random word from array function getRandomWord() { return words[Math.floor(Math.random() * words.length)]; } // Add word to DOM function addWordToDOM() { randomWord = getRandomWord(); word.innerHTML = randomWord; } // Update score function updateScore() { score++; scoreEl.innerHTML = score; } // Update time function updateTime() { time--; timeEl.innerHTML = time + 's'; if (time === 0) { clearInterval(timeInterval); // end game gameOver(); } } // Game over, show end screen function gameOver() { endgameEl.innerHTML = `

Time ran out

Your final score is ${score}

`; endgameEl.style.display = 'flex'; } addWordToDOM(); // Event listeners // Typing text.addEventListener('input', e => { const insertedText = e.target.value; if (insertedText === randomWord) { addWordToDOM(); updateScore(); // Clear e.target.value = ''; if (difficulty === 'hard') { time += 2; } else if (difficulty === 'medium') { time += 3; } else { time += 5; } updateTime(); } }); // Settings btn click settingsBtn.addEventListener('click', () => settings.classList.toggle('hide')); // Settings select settingsForm.addEventListener('change', e => { difficulty = e.target.value; localStorage.setItem('difficulty', difficulty); }); ================================================ FILE: typing-game/style.css ================================================ * { box-sizing: border-box; } body { background-color: #2c3e50; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; font-family: Verdana, Geneva, Tahoma, sans-serif; } button { cursor: pointer; font-size: 14px; border-radius: 4px; padding: 5px 15px; } select { width: 200px; padding: 5px; appearance: none; -webkit-appearance: none; -moz-appearance: none; border-radius: 0; background-color: #a7c5e3; } select:focus, button:focus { outline: 0; } .settings-btn { position: absolute; bottom: 30px; left: 30px; } .settings { position: absolute; top: 0; left: 0; width: 100%; background-color: rgba(0, 0, 0, 0.3); height: 70px; color: #fff; display: flex; align-items: center; justify-content: center; transform: translateY(0); transition: transform 0.3s ease-in-out; } .settings.hide { transform: translateY(-100%); } .container { background-color: #34495e; padding: 20px; border-radius: 4px; box-shadow: 0 3px 5px rgba(0, 0, 0, 0.3); color: #fff; position: relative; text-align: center; width: 500px; } h2 { background-color: rgba(0, 0, 0, 0.3); padding: 8px; border-radius: 4px; margin: 0 0 40px; } h1 { margin: 0; } input { border: 0; border-radius: 4px; font-size: 14px; width: 300px; padding: 12px 20px; margin-top: 10px; } .score-container { position: absolute; top: 60px; right: 20px; } .time-container { position: absolute; top: 60px; left: 20px; } .end-game-container { background-color: inherit; display: none; align-items: center; justify-content: center; flex-direction: column; position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }