diff --git a/challenges/challenge-cowsay-two/solution1.js b/challenges/challenge-cowsay-two/solution1.js index a7f2416b..616e6b2d 100644 --- a/challenges/challenge-cowsay-two/solution1.js +++ b/challenges/challenge-cowsay-two/solution1.js @@ -1,30 +1,35 @@ -// ================= -// Stripped down cowsayer CLI, -// no libraries -// https://nodejs.dev/learn/nodejs-accept-arguments-from-the-command-line -// ================= +// cowsay.js // 1. Accept arguments +const userInput = process.argv.slice(2).join(' '); + +// 2. Make speech bubble +function makeBubble(text) { + if (!text) text = "Moo!"; + const len = text.length; + const top = ' ' + '_'.repeat(len + 2); + const middle = `| ${text} |`; + const bottom = ' ' + '-'.repeat(len + 2); + return `${top}\n${middle}\n${bottom}`; +} -// how will you accept arguments? - -// 2. Make supplies for our speech bubble - -let topLine = '_'; -let bottomLine = '-'; -let saying = ''; - -// 3. Make a cow that takes a string +// 3. Make a simple cow +function makeCow() { + return ` + \\ ^__^ + \\ (oo)\\_______ + (__)\\ )\\/\\ + ||----w | + || || + `; +} +// 4. Combine bubble + cow function cowsay(saying) { -// how will you make the speech bubble contain the text? - -// where will the cow picture go? - -// how will you account for the parameter being empty? - + const bubble = makeBubble(saying); + const cow = makeCow(); + return bubble + cow; } -//4. Pipe argument into cowsay function and return a cow - -// how will you log this to the console? +// 5. Log to console +console.log(cowsay(userInput)); diff --git a/challenges/challenge-cowsay-two/solution2.js b/challenges/challenge-cowsay-two/solution2.js index 8aca8572..cec709da 100644 --- a/challenges/challenge-cowsay-two/solution2.js +++ b/challenges/challenge-cowsay-two/solution2.js @@ -1,18 +1,32 @@ -// ================= -// Stripped down cowsayer CLI, -// no libraries or arguments -// https://nodejs.dev/learn/accept-input-from-the-command-line-in-nodejs -// ================= +const readline = require('readline'); -// 1. Make a command line interface. +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); -// 2. Make supplies for our speech bubble - -// 3. Make a cow that takes a string +function makeBubble(text) { + if (!text) text = "Moo!"; + const len = text.length; + const top = ' ' + '_'.repeat(len + 2); + const middle = `| ${text} |`; + const bottom = ' ' + '-'.repeat(len + 2); + return `${top}\n${middle}\n${bottom}`; +} const cow = (saying) => { - // how did you make the cow before? -} + const bubble = makeBubble(saying); + const asciiCow = ` + \\ ^__^ + \\ (oo)\\_______ + (__)\\ )\\/\\ + ||----w | + || || + `; + return bubble + asciiCow; +}; -// 4. Use readline to get a string from the terminal -// (with a prompt so it's clearer what we want) \ No newline at end of file +rl.question("What should the cow say? ", (answer) => { + console.log(cow(answer)); + rl.close(); +}); diff --git a/challenges/challenge-weather-app/app.js b/challenges/challenge-weather-app/app.js new file mode 100644 index 00000000..3c0cda9c --- /dev/null +++ b/challenges/challenge-weather-app/app.js @@ -0,0 +1,97 @@ +// ==================== +// Mini-weather App JS +// ==================== + +// Your working API keys +const WEATHER_API_KEY = "13940029dbb0ac27fa8e76f770798f71"; +const UNSPLASH_ACCESS_KEY = "1EkJLQ64exkT3BxWkvtunY6nGsyoUpmmVOIycUfobC4"; + +// DOM elements +const photoEl = document.getElementById("photo"); +const thumbsEl = document.getElementById("thumbs"); +const conditionsEl = document.getElementById("conditions"); +const creditUserEl = document.getElementById("credit-user"); +const searchForm = document.getElementById("search"); +const searchInput = document.getElementById("search-tf"); + +// Fetch weather data from OpenWeather +async function getWeather(city = "London") { + const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${WEATHER_API_KEY}&units=metric`; + const res = await fetch(url); + if (!res.ok) throw new Error("Invalid API key or city"); + const data = await res.json(); + return data; +} + +// Fetch images from Unsplash +async function getImages(query) { + const url = `https://api.unsplash.com/search/photos?query=${query}&client_id=${UNSPLASH_ACCESS_KEY}&per_page=5`; + const res = await fetch(url); + const data = await res.json(); + return data.results; +} + +// Display main photo and thumbnails +function displayImages(images) { + if (!images.length) return; + + const mainImage = images[0]; + photoEl.style.backgroundImage = `url(${mainImage.urls.regular})`; + creditUserEl.textContent = mainImage.user.name; + creditUserEl.href = mainImage.user.links.html; + + thumbsEl.innerHTML = ""; + + images.forEach((img, index) => { + const thumb = document.createElement("div"); + thumb.classList.add("thumb"); + if (index === 0) thumb.classList.add("active"); + thumb.style.backgroundImage = `url(${img.urls.thumb})`; + thumb.dataset.full = img.urls.regular; + thumb.dataset.userName = img.user.name; + thumb.dataset.userLink = img.user.links.html; + + thumb.addEventListener("click", () => { + photoEl.style.backgroundImage = `url(${img.urls.regular})`; + creditUserEl.textContent = img.user.name; + creditUserEl.href = img.user.links.html; + + document.querySelectorAll(".thumb").forEach(t => t.classList.remove("active")); + thumb.classList.add("active"); + }); + + thumbsEl.appendChild(thumb); + }); +} + +// Display weather info +function displayWeather(weatherData) { + const description = weatherData.weather[0].description; + const temp = Math.round(weatherData.main.temp); + const cityName = weatherData.name; + conditionsEl.textContent = `${description}, ${temp}°C in ${cityName}`; +} + +// Update city +async function updateCity(city) { + try { + const weather = await getWeather(city); + displayWeather(weather); + const images = await getImages(weather.weather[0].description); + displayImages(images); + } catch (err) { + console.error(err); + conditionsEl.textContent = "Error fetching weather or images!"; + } +} + +// Load default city +updateCity("London"); + +// Handle search form +searchForm.addEventListener("submit", (e) => { + e.preventDefault(); + const city = searchInput.value.trim(); + if (city) updateCity(city); + searchInput.value = ""; +}); diff --git a/challenges/challenge-weather-app/index.html b/challenges/challenge-weather-app/index.html index b1add346..f25d1058 100644 --- a/challenges/challenge-weather-app/index.html +++ b/challenges/challenge-weather-app/index.html @@ -44,6 +44,7 @@

+ diff --git a/challenges/dog-photo-gallery/index.html b/challenges/dog-photo-gallery/index.html new file mode 100644 index 00000000..b8051e15 --- /dev/null +++ b/challenges/dog-photo-gallery/index.html @@ -0,0 +1,17 @@ + + + + + + Dog Photo Gallery + + + +

🐕 Random Dog Photo Gallery

+ + + + + + + diff --git a/challenges/dog-photo-gallery/script.js b/challenges/dog-photo-gallery/script.js new file mode 100644 index 00000000..2585f0bf --- /dev/null +++ b/challenges/dog-photo-gallery/script.js @@ -0,0 +1,33 @@ +const addDogBtn = document.getElementById("add-dog"); +const clearGalleryBtn = document.getElementById("clear-gallery"); +const gallery = document.getElementById("dog-gallery"); + +async function fetchDogImage() { + try { + const response = await fetch("https://dog.ceo/api/breeds/image/random"); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const data = await response.json(); + + // Create elements + const li = document.createElement("li"); + const img = document.createElement("img"); + img.src = data.message; + img.alt = "Cute Dog"; + + // Append to list + li.appendChild(img); + gallery.appendChild(li); + + } catch (error) { + console.error("Error fetching dog image:", error); + alert("🐶 Oops! Failed to fetch a dog image. Try again."); + } +} + +// Event listeners +addDogBtn.addEventListener("click", fetchDogImage); +clearGalleryBtn.addEventListener("click", () => { + gallery.innerHTML = ""; +}); diff --git a/challenges/dog-photo-gallery/style.css b/challenges/dog-photo-gallery/style.css new file mode 100644 index 00000000..180dd371 --- /dev/null +++ b/challenges/dog-photo-gallery/style.css @@ -0,0 +1,38 @@ +body { + font-family: Arial, sans-serif; + text-align: center; + padding: 20px; + } + + button { + margin: 10px; + padding: 10px 20px; + cursor: pointer; + border-radius: 8px; + border: none; + background: #4CAF50; + color: white; + font-size: 16px; + } + + button:hover { + background: #45a049; + } + + ul { + list-style: none; + padding: 0; + display: flex; + flex-wrap: wrap; + justify-content: center; + } + + li { + margin: 10px; + } + + img { + max-width: 200px; + border-radius: 12px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + } \ No newline at end of file diff --git a/challenges/unit-testing/katas-tdd/calculator/calculator.js b/challenges/unit-testing/katas-tdd/calculator/calculator.js new file mode 100644 index 00000000..b95ed43e --- /dev/null +++ b/challenges/unit-testing/katas-tdd/calculator/calculator.js @@ -0,0 +1,14 @@ +export function add(numbers) { + if (numbers === "") return 0; + + const parts = numbers.split(",").map(Number); + const negatives = parts.filter((num) => num < 0); + + if (negatives.length > 0) { + throw new Error(`negatives not allowed: ${negatives.join(",")}`); + } + + return parts + .filter((num) => num <= 1000) // ignore > 1000 + .reduce((sum, num) => sum + num, 0); +} \ No newline at end of file diff --git a/challenges/unit-testing/katas-tdd/calculator/calculator.test.js b/challenges/unit-testing/katas-tdd/calculator/calculator.test.js new file mode 100644 index 00000000..e1af88ad --- /dev/null +++ b/challenges/unit-testing/katas-tdd/calculator/calculator.test.js @@ -0,0 +1,15 @@ +import { add } from "./calculator"; + +describe("Calculator", () => { + test("should return 0 for empty string", () => { + expect(add("")).toBe(0); + }); + + test("should return the number itself when only one number is given", () => { + expect(add("5")).toBe(5); + }); + + test("should return sum of two numbers", () => { + expect(add("3,6")).toBe(9); + }); +}); diff --git a/challenges/unit-testing/katas-tdd/password-verifier/verify.js b/challenges/unit-testing/katas-tdd/password-verifier/verify.js new file mode 100644 index 00000000..54cdaba0 --- /dev/null +++ b/challenges/unit-testing/katas-tdd/password-verifier/verify.js @@ -0,0 +1,15 @@ +export function verify(password) { + if (!password || password.length < 8) { + return "Password rejected"; + } + + if (!/[A-Z]/.test(password)) { + return "Password rejected"; + } + + if (!/[0-9]/.test(password)) { + return "Password rejected"; + } + + return "Password accepted"; +} diff --git a/challenges/unit-testing/katas-tdd/password-verifier/verify.test.js b/challenges/unit-testing/katas-tdd/password-verifier/verify.test.js new file mode 100644 index 00000000..9af6a931 --- /dev/null +++ b/challenges/unit-testing/katas-tdd/password-verifier/verify.test.js @@ -0,0 +1,25 @@ +import { verify } from "./verify.js"; + +describe("Password Verifier", () => { + + test("rejects password shorter than 8 characters", () => { + expect(verify("Ab1")).toBe("Password rejected"); + }); + + test("rejects null password", () => { + expect(verify(null)).toBe("Password rejected"); + }); + + test("rejects password without uppercase letter", () => { + expect(verify("password1")).toBe("Password rejected"); + }); + + test("rejects password without number", () => { + expect(verify("Password")).toBe("Password rejected"); + }); + + test("accepts valid password", () => { + expect(verify("Password1")).toBe("Password accepted"); + }); + +}); diff --git a/challenges/unit-testing/katas-tdd/roman-numerals/convert-to-new-roman.js b/challenges/unit-testing/katas-tdd/roman-numerals/convert-to-new-roman.js index 0f2fe4b5..ec338ce3 100644 --- a/challenges/unit-testing/katas-tdd/roman-numerals/convert-to-new-roman.js +++ b/challenges/unit-testing/katas-tdd/roman-numerals/convert-to-new-roman.js @@ -1,3 +1,20 @@ -function convertToNewRoman(n) {} +function convertToNewRoman(n) { + // Step 1: simple mapping for 1–10 as example + const romanNumerals = { + 1: "I", + 2: "II", + 3: "III", + 4: "IV", + 5: "V", + 6: "VI", + 7: "VII", + 8: "VIII", + 9: "IX", + 10: "X", + }; + + return romanNumerals[n] || ""; +} module.exports = convertToNewRoman; + diff --git a/challenges/unit-testing/katas-tdd/roman-numerals/convert-to-new-roman.test.js b/challenges/unit-testing/katas-tdd/roman-numerals/convert-to-new-roman.test.js index ae49f737..2d6c2c00 100644 --- a/challenges/unit-testing/katas-tdd/roman-numerals/convert-to-new-roman.test.js +++ b/challenges/unit-testing/katas-tdd/roman-numerals/convert-to-new-roman.test.js @@ -1,7 +1,25 @@ -let convertToNewRoman = require("./convert-to-new-roman"); +const convertToNewRoman = require("convert-to-new-roman.js"); -test("returns I if passed 1 as an argument", function () { +test("returns I if passed 1 as an argument", () => { // Arrange + const input = 1; + // Act + const result = convertToNewRoman(input); + // Assert + expect(result).toBe("I"); +}); + +test("returns V if passed 5 as an argument", () => { + const input = 5; + const result = convertToNewRoman(input); + expect(result).toBe("V"); }); + +test("returns X if passed 10 as an argument", () => { + const input = 10; + const result = convertToNewRoman(input); + expect(result).toBe("X"); +}); + diff --git a/challenges/unit-testing/katas-tdd/roman-numerals/convert-to-old-roman.js b/challenges/unit-testing/katas-tdd/roman-numerals/convert-to-old-roman.js index f7f0d06d..866d64c2 100644 --- a/challenges/unit-testing/katas-tdd/roman-numerals/convert-to-old-roman.js +++ b/challenges/unit-testing/katas-tdd/roman-numerals/convert-to-old-roman.js @@ -1,3 +1,20 @@ -function convertToOldRoman(n) {} +function convertToOldRoman(n) { + // Step 1: simple mapping for 1–10 + const oldRomanNumerals = { + 1: "I", + 2: "II", + 3: "III", + 4: "IIII", + 5: "V", + 6: "VI", + 7: "VII", + 8: "VIII", + 9: "VIIII", + 10: "X", + }; + + return oldRomanNumerals[n] || ""; +} module.exports = convertToOldRoman; + diff --git a/challenges/unit-testing/katas-tdd/roman-numerals/convert-to-old-roman.test.js b/challenges/unit-testing/katas-tdd/roman-numerals/convert-to-old-roman.test.js index b9a43869..dcaa8c6a 100644 --- a/challenges/unit-testing/katas-tdd/roman-numerals/convert-to-old-roman.test.js +++ b/challenges/unit-testing/katas-tdd/roman-numerals/convert-to-old-roman.test.js @@ -1,7 +1,22 @@ -let convertToOldRoman = require("./convert-to-old-roman"); +const convertToOldRoman = require("convert-to-old-roman.js"); -test("returns I if passed 1 as an argument", function () { - // Arrange - // Act - // Assert +test("returns I if passed 1 as an argument", () => { + expect(convertToOldRoman(1)).toBe("I"); }); + +test("returns IIII if passed 4 as an argument", () => { + expect(convertToOldRoman(4)).toBe("IIII"); +}); + +test("returns V if passed 5 as an argument", () => { + expect(convertToOldRoman(5)).toBe("V"); +}); + +test("returns VIII if passed 8 as an argument", () => { + expect(convertToOldRoman(8)).toBe("VIII"); +}); + +test("returns X if passed 10 as an argument", () => { + expect(convertToOldRoman(10)).toBe("X"); +}); + diff --git a/challenges/unit-testing/passing-tests/car-sales/car-sales.js b/challenges/unit-testing/passing-tests/car-sales/car-sales.js index e8bb8511..ccbce1b1 100644 --- a/challenges/unit-testing/passing-tests/car-sales/car-sales.js +++ b/challenges/unit-testing/passing-tests/car-sales/car-sales.js @@ -1,3 +1,20 @@ -function sales(carsSold) {} + +function sales(carsSold) { + const totals = {}; + + for (const car of carsSold) { + const make = car.make; + const price = car.price; + + if (!totals[make]) { + totals[make] = 0; + } + + totals[make] += price; + } + + return totals; +} module.exports = sales; + diff --git a/challenges/unit-testing/passing-tests/car-sales/car-sales.test.js b/challenges/unit-testing/passing-tests/car-sales/car-sales.test.js index 07139fc0..9f4b3233 100644 --- a/challenges/unit-testing/passing-tests/car-sales/car-sales.test.js +++ b/challenges/unit-testing/passing-tests/car-sales/car-sales.test.js @@ -1,7 +1,7 @@ -let sales = require("./car-sales"); +const sales = require("./car-sales"); -test("Car sales", function () { - let carsSold = [ +test("Car sales", () => { + const carsSold = [ { make: "Ford", model: "Fiesta", colour: "Red", price: 5999 }, { make: "Land Rover", model: "Defender", colour: "Muddy", price: 12000 }, { make: "Toyota", model: "Prius", colour: "Silver", price: 6500 }, @@ -11,14 +11,14 @@ test("Car sales", function () { { make: "Ford", model: "Fiesta", colour: "Green", price: 2000 }, ]; - let totals = { + const totals = { Ford: 22999, Honda: 8000, "Land Rover": 21000, Toyota: 6500, }; - let output = sales(carsSold); + const output = sales(carsSold); expect(output).toEqual(totals); }); diff --git a/challenges/unit-testing/passing-tests/factorial/factorial.js b/challenges/unit-testing/passing-tests/factorial/factorial.js index fefa4368..17d74abc 100644 --- a/challenges/unit-testing/passing-tests/factorial/factorial.js +++ b/challenges/unit-testing/passing-tests/factorial/factorial.js @@ -1,13 +1,14 @@ -// int is an integer -// a factorial is the product of all non-negative integers -// less than or equal to the iniital number. +function factorial(int) { + if (int === 0) return 1; -// for example the factorial of 5 is 120 -// 120 = 1 * 2 * 3 * 4 * 5 + let result = 1; + for (let i = 1; i <= int; i++) { + result *= i; + } -// calculate and return the factorial of int -// note: factorial of 0 is 1 - -function factorial(int) {} + return result; +} module.exports = factorial; + + diff --git a/challenges/unit-testing/passing-tests/get-average/get-average.js b/challenges/unit-testing/passing-tests/get-average/get-average.js index b5588a57..2a2bb9e3 100644 --- a/challenges/unit-testing/passing-tests/get-average/get-average.js +++ b/challenges/unit-testing/passing-tests/get-average/get-average.js @@ -1,7 +1,13 @@ -// the input is an array of numbers and strings -// return the average of all the numbers -// be sure to exclude the strings +function average(numbers) { + const numsOnly = numbers.filter(n => typeof n === "number"); -function average(numbers) {} + if (numsOnly.length === 0) return 0; + + const sum = numsOnly.reduce((acc, n) => acc + n, 0); + + return Math.floor(sum / numsOnly.length); +} module.exports = average; + + diff --git a/challenges/unit-testing/writing-tests/largest-number/largest-number.js b/challenges/unit-testing/writing-tests/largest-number/largest-number.js index 4036dd74..ba86c168 100644 --- a/challenges/unit-testing/writing-tests/largest-number/largest-number.js +++ b/challenges/unit-testing/writing-tests/largest-number/largest-number.js @@ -1,6 +1,6 @@ function getLargestNumber(array) { let largestNumber = array[0]; - for (let i = 0; i < array.length; i++) { + for (let i = 1; i < array.length; i++) { if (array[i] > largestNumber) { largestNumber = array[i]; } @@ -9,3 +9,4 @@ function getLargestNumber(array) { } module.exports = getLargestNumber; + diff --git a/challenges/unit-testing/writing-tests/largest-number/largest-number.test.js b/challenges/unit-testing/writing-tests/largest-number/largest-number.test.js index ce674209..ffa01d88 100644 --- a/challenges/unit-testing/writing-tests/largest-number/largest-number.test.js +++ b/challenges/unit-testing/writing-tests/largest-number/largest-number.test.js @@ -1,13 +1,11 @@ -let getLargestNumber = require("./largest-number"); +const getLargestNumber = require("./largest-number"); -test("returns largest number in array", function () { - // Arrange - // Act - // Assert -}); +test("returns largest number in array", () => { + const input = [3, 21, 88, 4, 36]; + const copy = [...input]; // to check original array is unchanged -// example -// input: [3, 21, 88, 4, 36]; -// expected: 88; + const output = getLargestNumber(input); -// also test that the original array hasn't changed + expect(output).toBe(88); // largest number + expect(input).toEqual(copy); // original array unchanged +}); diff --git a/challenges/unit-testing/writing-tests/remove-vowels/remove-vowels-in-array.js b/challenges/unit-testing/writing-tests/remove-vowels/remove-vowels-in-array.js index 6f4e06bf..307f8359 100644 --- a/challenges/unit-testing/writing-tests/remove-vowels/remove-vowels-in-array.js +++ b/challenges/unit-testing/writing-tests/remove-vowels/remove-vowels-in-array.js @@ -1,16 +1,8 @@ -let removeVowels = require("./remove-vowels"); +const removeVowels = require("./remove-vowels"); function removeVowelsFromWords(words) { - let result = words.map(function (word) { - return removeVowels(word); - }); - - return result; + return words.map(word => removeVowels(word)); } module.exports = removeVowelsFromWords; -/* - input: ["Irina", "Etza", "Daniel"] - expected output: ["rn", "tz", "Dnl"] -*/ diff --git a/challenges/unit-testing/writing-tests/remove-vowels/remove-vowels-in-array.test.js b/challenges/unit-testing/writing-tests/remove-vowels/remove-vowels-in-array.test.js index ee739e22..2f5f48fc 100644 --- a/challenges/unit-testing/writing-tests/remove-vowels/remove-vowels-in-array.test.js +++ b/challenges/unit-testing/writing-tests/remove-vowels/remove-vowels-in-array.test.js @@ -1,11 +1,11 @@ -let removeVowelsFromWords = require("./remove-vowels-in-array"); +const removeVowelsFromWords = require("./remove-vowels-in-array"); -test("remove vowels from all words in array", function () { - // Arrange - // Act - // Assert +test("remove vowels from all words in array", () => { + const input = ["Irina", "Etza", "Daniel"]; + const expected = ["rn", "tz", "Dnl"]; + + const output = removeVowelsFromWords(input); + + expect(output).toEqual(expected); }); -// example -// input: ["Irina", "Etza", "Daniel"] -// expected output: ["rn", "tz", "Dnl"] diff --git a/challenges/unit-testing/writing-tests/remove-vowels/remove-vowels.js b/challenges/unit-testing/writing-tests/remove-vowels/remove-vowels.js index e5bb67ba..514c1722 100644 --- a/challenges/unit-testing/writing-tests/remove-vowels/remove-vowels.js +++ b/challenges/unit-testing/writing-tests/remove-vowels/remove-vowels.js @@ -19,11 +19,3 @@ function removeVowels(word) { } module.exports = removeVowels; - -/* - Let's trace this piece of code - what is the value of result with this input - - let result = removeVowels('samuel'); - - what is the value of result? -*/