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!
Show Rules
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.
Close
================================================
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
Add User 👱♂️
Double Money 💰
Show Only Millionaires 💵
Sort by Richest ↓
Calculate entire Wealth 🧮
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
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
)} x
`;
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
================================================
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
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 = `
${data.data
.map(
song => `
${song.artist.name} - ${song.title}
Get Lyrics
`
)
.join('')}
`;
if (data.prev || data.next) {
more.innerHTML = `
${
data.prev
? `Prev `
: ''
}
${
data.next
? `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
================================================
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}
`
)
.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.strCategory ? `
${meal.strCategory}
` : ''}
${meal.strArea ? `
${meal.strArea}
` : ''}
${meal.strInstructions}
Ingredients
${ingredients.map(ing => `${ing} `).join('')}
`;
}
// 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
Clear Cards
Memory Cards
Add New Card
Add New Card
Question
Answer
Add 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 = `
`;
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
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
Lifetime Access
30 Day Money Back
Tailored Customer Support
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?
Register with us to get offers, support and more
================================================
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
Pick a movie:
Avengers: Endgame ($10)
Joker ($12)
Toy Story 4 ($8)
The Lion King ($9)
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
================================================
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
================================================
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
================================================
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 = `
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
Check Order
================================================
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}
`;
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
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}
Play Again
`;
} 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
Toggle Text Box
================================================
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}
`;
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}
Reload
`;
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;
}