Built at: 2024-10-16T09:55:25.150Z Skip to content

part2e

##  Adding styles to React app

add src/index.css
h1 {
color: green;
font-style: italic;
}
index.js
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

revise src/components/Note.js, add className
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>
);
};
revise src/index.css
.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

Notification.js
const Notification = ({ message }) => {
if (message === null) {
return null;
}
return <div className="error">{message}</div>;
};
App.js
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>
);
};
index.css
.error {
color: red;
background: lightgrey;
font-size: 20px;
border-style: solid;
border-radius: 5px;
padding: 10px;
margin-bottom: 10px;
}
change the error message from alert to Notification
// 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

general css styles
{
color: green;
font-style: italic;
font-size: 16px;
}
inline css styles
{
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

src/components/Footer.js
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

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

src/index.css
.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;
}
src/index.js
import "./index.css";
// ...
add src/components/Notification.js
import React from "react";
const Notification = ({ message }) => {
if (message === null) {
return null;
}
return <div className="info">{message}</div>;
};
export default Notification;
add src/components/NotificationError.js
import React from "react";
const NotificationError = ({ message }) => {
if (message === null) {
return null;
}
return <div className="error">{message}</div>;
};
export default NotificationError;
App.js
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 null
if (!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

Terminal window
pnpm i axios
pnpm i lodash
App.js
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.
Terminal window
pnpm create vite@latest
choose react
pnpm i rollup@4.18.0
pnpm i axios
pnpm i lodash
pnpm dev
.env.local
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

App.js
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;