part2e
## Adding styles to React app
h1 { color: green; font-style: italic;}
import "./index.css";
Check the Header style changing at http://localhost:3000
li { color: grey; padding-top: 3px; font-size: 25px;}
check the note style changing at http://localhost:3000
const Note = ({ note, toggleImportance }) => { const label = note.important ? "make not important" : "make important";
return ( <li className="note"> {note.content} <button onClick={toggleImportance}>{label}</button> </li> );};
.note { color: blue; padding-top: 5px; font-size: 15px;}
check the note style changing at http://localhost:3000
imporved error message
Add Notification component and import into App.js
const Notification = ({ message }) => { if (message === null) { return null; }
return <div className="error">{message}</div>;};
import Notification from "src/components/Notification";
// ...
const App = () => { const [notes, setNotes] = useState([]); const [newNote, setNewNote] = useState(""); const [showAll, setShowAll] = useState(true);
const [errorMessage, setErrorMessage] = useState("some error happened...");
// ...
return ( <div> <h1>Notes</h1> <Notification message={errorMessage} /> <div> <button onClick={() => setShowAll(!showAll)}> show {showAll ? "important" : "all"} </button> </div> // ... </div> );};
.error { color: red; background: lightgrey; font-size: 20px; border-style: solid; border-radius: 5px; padding: 10px; margin-bottom: 10px;}
// App.js
const toggleImportanceOf = (id) => { const note = notes.find((n) => n.id === id); const changedNote = { ...note, important: !note.important };
noteService .update(id, changedNote) .then((returnedNote) => { setNotes(notes.map((note) => (note.id !== id ? note : returnedNote))); }) .catch((error) => { setErrorMessage(`Note '${note.content}' was already removed from server`); setTimeout(() => { setErrorMessage(null); }, 5000); setNotes(notes.filter((n) => n.id !== id)); });};
Inline styles
{ color: green; font-style: italic; font-size: 16px;}
{ color: 'green', fontStyle: 'italic', fontSize: 16}
- Hyphenated (kebab case) CSS properties are written in camelCase.
- CSS property is defined as a separate property of the JavaScript object.
- Numeric values for pixels can be simply defined as integers.
See how the inline styles working.
Add Footer component
const Footer = () => { const footerStyle = { color: "green", fontStyle: "italic", fontSize: 16, }; return ( <div style={footerStyle}> <br /> <em> Note app, Department of Computer Science, University of Helsinki 2024 </em> </div> );};
And import into App.js
const App = () => { // ...
return ( <div> <h1>Notes</h1> <Notification message={errorMessage} /> // ... <Footer /> </div> );};
Check http://localhost:3000
Exercises 2.16 - 2.17
2.16 The phonebook step 11
Add notification when a person is added or number changed in addPerson
2.17*: Phonebook step 12
Open your application in two browsers. If you delete a person in browser 1 a short while before attempting to change the person’s phone number in browser 2, you will get the 404 error messages
Fix by showing the message in Notification
The messages shown for successful and unsuccessful events should look different
My solutions
My solutions:
(Please complete your own solutions before click here.)
2.16 The phonebook step 11
Add notification when a person is added or number changed in addPerson
.info { color: green; background: lightgrey; font-size: 20px; border-style: solid; border-radius: 5px; padding: 10px; margin-bottom: 10px;}
.error { color: red; background: lightgrey; font-size: 20px; border-style: solid; border-radius: 5px; padding: 10px; margin-bottom: 10px;}
import "./index.css";
// ...
import React from "react";
const Notification = ({ message }) => { if (message === null) { return null; }
return <div className="info">{message}</div>;};
export default Notification;
import React from "react";
const NotificationError = ({ message }) => { if (message === null) { return null; }
return <div className="error">{message}</div>;};
export default NotificationError;
import { useState, useEffect } from "react";import personService from "./services/persons";import Filter from "./components/Filter";import PersonForm from "./components/PersonForm";import Notification from "./components/Notification";import NotificationError from "./components/NotificationError";import Persons from "./components/Persons";var _ = require("lodash");
const App = () => { const [persons, setPersons] = useState([]); const [newName, setNewName] = useState(""); const [newNumber, setNewNumber] = useState(""); const [filterName, setFilterName] = useState(""); const [infoMessage, setInfoMessage] = useState(null); const [errorMessage, setErrorMessage] = useState(null);
useEffect(() => { personService.getAll().then((initialPersons) => { setPersons(initialPersons); }); }, []); // console.log('render', persons.length, 'persons')
const handleNameChange = (event) => { event.preventDefault(); setNewName(event.target.value); }; const handleNumberChange = (event) => { event.preventDefault(); setNewNumber(event.target.value); };
const addPerson = (event) => { event.preventDefault();
const personObject = { name: newName, number: newNumber, id: String(persons.length + 1), };
const existPersonArray = persons.filter((person) => _.includes(person, newName) ); // console.log(existPersonArray)
if (existPersonArray.length > 0) { // alert(`${newName} is already added to phonebook`)
const existPerson = existPersonArray[0]; // console.log(existPerson)
if ( window.confirm( `${newName} is already added to phonebook, replace the old number with a new one?` ) ) { const id = existPerson.id; personService .update(id, personObject) .then((returnedPerson) => { setPersons( persons.map((person) => person.id !== id ? person : returnedPerson ) ); setNewName(""); setNewNumber(""); setInfoMessage(`Updated ${returnedPerson.name}`); setTimeout(() => { setInfoMessage(null); }, 5000); }) .catch((error) => { setErrorMessage( `Information of ${existPerson.name} has already been removed from server` ); setTimeout(() => { setErrorMessage(null); }, 5000); setPersons(persons.filter((person) => person.id !== id)); }); } } else { personService.create(personObject).then((returnedPerson) => { setPersons(persons.concat(returnedPerson)); setNewName(""); setNewNumber(""); setInfoMessage(`Added ${returnedPerson.name}`); setTimeout(() => { setInfoMessage(null); }, 5000); }); } };
const handleFilterName = (event) => { event.preventDefault(); setFilterName(event.target.value); };
const personsToShow = persons.filter((person) => _.includes(person.name.toLowerCase(), filterName.toLowerCase()) );
const handlePersonDelete = (person) => { // console.log(`deleting person ${id}`)
if (window.confirm(`delete ${person.name} ?`)) { personService.deleteId(person.id).then((responsePerson) => { setPersons(persons.filter((n) => n.id !== responsePerson.id)); }); } };
return ( <div> <h2>Phonebook</h2> <Notification message={infoMessage} /> <NotificationError message={errorMessage} /> <Filter filterName={filterName} onChange={handleFilterName} /> <h2>Add a new</h2> <PersonForm addPerson={addPerson} newName={newName} newNumber={newNumber} handleNameChange={handleNameChange} handleNumberChange={handleNumberChange} /> <h2>Numbers</h2> <Persons personsToShow={personsToShow} handleClick={handlePersonDelete} /> </div> );};
export default App;
Couple of important remarks
error on initialNotes is null
If the notes is set as null initially as below, the app breaks down.
const [notes, setNotes] = useState(null);
So need to check it at the first run and return null if no note, and then the useEffect will fetch the notes, setNotes and then start the next rendering:
useEffect(() => { noteService.getAll().then((initialNotes) => { setNotes(initialNotes); });}, []);
// do not render anything if notes is still nullif (!notes) { return null;}
### useEffect second parameter
Currently the second parameter is [], it’s empty, so the useEffect will run only once.
If there is a value in the array, the useEffect will run whenever the value changes.
Exercises 2.18 - 2.20
2.18* Data for countries step 1
Get data from https://studies.cs.helsinki.fi/restcountries/, make a search filter.
If there are too many (over 10) countries that match the query, then the user is prompted to make their query more specific
If there are ten or fewer countries, but more than one, then all countries matching the query are shown
When there is only one country matching the query, then the basic data of the country (eg. capital and area), its flag and the languages spoken are shown
2.19* Data for countries step 2
Add show button after each of the showing countries.
2.20* Data for countries step 3
Add whether info for the country info page. https://openweathermap.org
The API v2.5 (by city name and metric):
https://openweathermap.org/current#name
https://openweathermap.org/current#data
https://samples.openweathermap.org/data/2.5/weather?q=London&appid=b1b15e88fa797225412429c1c50c122a1
https://api.openweathermap.org/data/2.5/weather?q=London&appid=da9c70187b1bc9cc7a181b87e34f23ae
https://api.openweathermap.org/data/2.5/weather?q=London&appid=da9c70187b1bc9cc7a181b87e34f23ae&units=metric
name to coordinates(lat, lon) https://openweathermap.org/api/geocoding-api#direct
Show temperature, whether icon, wind.
My solutions
My solutions:
(Please complete your own solutions before click here.)
2.18
2.19
pnpm i axiospnpm i lodash
import { useState, useEffect } from "react";import axios from "axios";var _ = require("lodash");
const ShowCountryInfo = (props) => { const { countryToShow } = props; console.log("ShowCountryInfo", countryToShow);
const theCountry = countryToShow[0];
return ( <div> <h2>{theCountry.name.common}</h2> <p>capital {theCountry.capital[0]}</p> <p>area {theCountry.area}</p> <div> <strong>Languages:</strong> <ul> {Object.keys(theCountry.languages).map((languageKey) => ( <li key={languageKey}>{theCountry.languages[languageKey]}</li> ))} </ul> </div> <img src={theCountry.flags.png} alt={theCountry.flags.alt} /> </div> );};
const CountriesList = (props) => { // console.log(props) const { countriesToShow, countries, handleShow } = props;
if (countriesToShow.length > 10) { return "Too many matches, specify another filter"; } if (countriesToShow.length > 1) { return countriesToShow.map((country) => { // console.log(country) return ( <div key={country.common}> {country.common} <button onClick={() => handleShow(country.common)}>show</button> </div> ); }); } if (countriesToShow.length === 1) { // console.log(countriesToShow[0]) const countryToShow = countries.filter((country) => _.includes( country.name.common.toLowerCase(), String(countriesToShow[0].common).toLowerCase() ) ); // console.log(countryToShow) return <ShowCountryInfo countryToShow={countryToShow} />; }
return "No Country fit the search";};
const App = () => { const [countries, setCountries] = useState([]); const [filterName, setFilterName] = useState("");
// const baseUrl = "https://studies.cs.helsinki.fi/restcountries/" useEffect(() => { // axios.get(baseUrl + "api/all") // axios.get("https://studies.cs.helsinki.fi/restcountries/api/all") axios.get("/dataOfCountries.json").then((response) => { const respCountries = response.data; // console.log(respCountries[0]) setCountries(respCountries); // console.log(countries) }); }, []);
if (countries.length === 0) return null; // console.log(countries)
const countriesNameArray = countries.map((country) => country.name); // console.log(countriesNameArray)
const handleFilterName = (event) => { event.preventDefault(); setFilterName(event.target.value); };
const countriesToShow = countriesNameArray.filter((country) => _.includes(country.common.toLowerCase(), filterName.toLowerCase()) ); // console.log(countriesToShow)
const handleShow = (countryCommon) => { // console.log(countryCommon) setFilterName(countryCommon); };
return ( <div> <h1>Data for countries</h1> <p> find countries <input value={filterName} onChange={handleFilterName} /> </p> <CountriesList countriesToShow={countriesToShow} countries={countries} handleShow={handleShow} /> </div> );};
export default App;
2.20
Solving the env issue:
- restart the project with pnpm create vite@lates, and select react.
- set env variable in .env or .env.local, add this env file in .gitignore, do not put this to git
- the variable should start with VITE_(according to vite’s doc), so that we can get the variable in our program.
pnpm create vite@latestchoose reactpnpm i rollup@4.18.0pnpm i axiospnpm i lodashpnpm dev
VITE_OPENWEATHERMAP_API_KEY={...mysecrete, replace with yours}
get https://studies.cs.helsinki.fi/restcountries/api/all and put in public folder filename dataOfCountries.json
import { useState, useEffect } from "react";import axios from "axios";
// var _ = require("lodash");import _ from "lodash";
console.log("apikey", import.meta.env.VITE_OPENWEATHERMAP_API_KEY);
const ShowCountryInfo = (props) => { const [weatherJson, setWeatherJson] = useState(null); const [weatherIconSrc, setWeatherIconSrc] = useState(""); const { countryToShow } = props; // console.log("ShowCountryInfo", countryToShow)
const theCountry = countryToShow[0]; const capital = theCountry.capital[0]; const weatherApi = `https://api.openweathermap.org/data/2.5/weather?q=${capital}&appid=${ import.meta.env.VITE_OPENWEATHERMAP_API_KEY }&units=metric`; console.log("weatherApi", weatherApi);
useEffect(() => { axios.get(weatherApi).then((response) => { // const iconName = response.data.weather[0].icon const iconSrc = "https://openweathermap.org/img/wn/" + response.data.weather[0].icon + "@2x.png"; setWeatherIconSrc(iconSrc); setWeatherJson(response.data); }); }, []);
if (!weatherJson) return null;
return ( <div> <h2>{theCountry.name.common}</h2> <p>capital {theCountry.capital[0]}</p> <p>area {theCountry.area}</p> <div> <strong>Languages:</strong> <ul> {/* https://bobbyhadz.com/blog/react-loop-through-object */} {Object.keys(theCountry.languages).map((languageKey) => ( <li key={languageKey}>{theCountry.languages[languageKey]}</li> ))} </ul> </div> <img src={theCountry.flags.png} alt={theCountry.flags.alt} /> <p> <strong>Weath in {theCountry.capital[0]}</strong> </p> <p> temperature {weatherJson.main.temp} Celcius <br /> <img src={weatherIconSrc} alt={weatherJson.wind.speed} /> <br /> wind {weatherJson.wind.speed} m/s </p> </div> );};
const CountriesList = (props) => { // console.log(import.meta.env.VITE_OPENWEATHERMAP_API_KEY) // console.log(process.env) // console.log(props)
const { countriesToShow, countries, handleShow } = props;
if (countriesToShow.length > 10) { return "Too many matches, specify another filter"; } if (countriesToShow.length > 1) { return countriesToShow.map((country) => { // console.log(country) return ( <div key={country.common}> {country.common} <button onClick={() => handleShow(country.common)}>show</button> </div> ); }); } if (countriesToShow.length === 1) { // console.log(countriesToShow[0]) const countryToShow = countries.filter((country) => _.includes( country.name.common.toLowerCase(), String(countriesToShow[0].common).toLowerCase() ) ); // console.log(countryToShow) return <ShowCountryInfo countryToShow={countryToShow} />; }
return "No Country fit the search";};
const App = () => { const [countries, setCountries] = useState([]); const [filterName, setFilterName] = useState("");
// const baseUrl = "https://studies.cs.helsinki.fi/restcountries/" useEffect(() => { // axios.get(baseUrl + "api/all") // axios.get("https://studies.cs.helsinki.fi/restcountries/api/all") axios.get("/dataOfCountries.json").then((response) => { const respCountries = response.data; // console.log(respCountries[0]) setCountries(respCountries); // console.log(countries) }); }, []);
if (!countries) return null; if (countries.length === 0) return null; // console.log(countries)
const countriesNameArray = countries.map((country) => country.name); // console.log(countriesNameArray)
const handleFilterName = (event) => { event.preventDefault(); setFilterName(event.target.value); };
const countriesToShow = countriesNameArray.filter((country) => _.includes(country.common.toLowerCase(), filterName.toLowerCase()) ); // console.log(countriesToShow)
const handleShow = (countryCommon) => { // console.log(countryCommon) setFilterName(countryCommon); };
return ( <div> <h1>Data for countries</h1> <p> find countries <input value={filterName} onChange={handleFilterName} /> </p> <CountriesList countriesToShow={countriesToShow} countries={countries} handleShow={handleShow} /> </div> );};
export default App;