diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-02-20 16:11:50 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-02-20 16:15:08 +0100 |
| commit | 73a5c7fae9ffbe9ada721148c8c454a643aceebe (patch) | |
| tree | c8fad013ed9b5dd589add87f8d45cf02bbfc6e91 /public/projects/js-small-apps/users-list | |
| parent | b01239fbdcc5bbc5921f73ec0e8fee7bedd5c8e8 (diff) | |
chore!: restructure repo
I separated public files from the config/dev files. It improves repo
readability.
I also moved dotenv helper to public/inc directory and extract the
Matomo tracker in the same directory.
Diffstat (limited to 'public/projects/js-small-apps/users-list')
| -rw-r--r-- | public/projects/js-small-apps/users-list/README.md | 18 | ||||
| -rw-r--r-- | public/projects/js-small-apps/users-list/app.js | 187 | ||||
| -rw-r--r-- | public/projects/js-small-apps/users-list/index.html | 29 | ||||
| -rw-r--r-- | public/projects/js-small-apps/users-list/style.css | 104 |
4 files changed, 338 insertions, 0 deletions
diff --git a/public/projects/js-small-apps/users-list/README.md b/public/projects/js-small-apps/users-list/README.md new file mode 100644 index 0000000..4c10232 --- /dev/null +++ b/public/projects/js-small-apps/users-list/README.md @@ -0,0 +1,18 @@ +# Users list + +An users list implementation using Javascript. + +The implementation is inspired by the [CauseEffect app from @florinpop17 repo](https://github.com/florinpop17/app-ideas/blob/master/Projects/1-Beginner/Cause-Effect-App.md). + +However, I made small changes: + +- Instead of hard coding the users objects, this implementation uses `fetch()` to retrieve data from an API. +- I implemented "routing": user info can be displayed if URL contains the user parameter. + +## Preview + +You can see a live preview here: https://demo.armandphilippot.com/#users-list + +## License + +This project is open-source and available under the [MIT License](../LICENSE). diff --git a/public/projects/js-small-apps/users-list/app.js b/public/projects/js-small-apps/users-list/app.js new file mode 100644 index 0000000..c9f998d --- /dev/null +++ b/public/projects/js-small-apps/users-list/app.js @@ -0,0 +1,187 @@ +/** + * Retrieve data from an API. + * @param {String} api - The API url. + * @returns {Promise} The result from api fetching. + */ +async function fetchData(api) { + const headers = new Headers(); + const init = { + method: "GET", + headers, + mode: "cors", + cache: "default", + }; + const response = await fetch(api, init); + const result = await response.json(); + return result; +} + +/** + * Create a list item with a link. + * @param {Object} data - An object containing an id and an username. + * @returns {HTMLElement} A list item containing a link. + */ +function getListItem(data) { + const item = document.createElement("li"); + const link = document.createElement("a"); + link.textContent = data.username; + link.href = `?user=${data.id}`; + link.id = `user-${data.id}`; + link.classList.add("users-list__link"); + item.appendChild(link); + item.classList.add("users-list__item"); + return item; +} + +/** + * Print the users list. + * @param {Object[]} users - An array of user object. + */ +function printUsersList(users) { + const list = document.querySelector(".users-list"); + users.map((user) => list.appendChild(getListItem(user))); +} + +/** + * Create a description term element. + * @param {String} body - The description term body. + * @returns {HTMLElement} The description term element. + */ +function createUserInfoDt(body) { + const dt = document.createElement("dt"); + dt.classList.add("user-info__label"); + dt.textContent = body; + return dt; +} + +/** + * Create a description details element. + * @param {String} body - The description details body. + * @param {Boolean} [hasHTML] - True if body contains HTML; false otherwise. + * @returns {HTMLElement} The description details element. + */ +function createUserInfoDd(body, hasHTML = false) { + const dd = document.createElement("dd"); + dd.classList.add("user-info__content"); + hasHTML ? (dd.innerHTML = body) : (dd.textContent = body); + return dd; +} + +/** + * Get the markup to display info for a given user. + * @param {Object} user - An user with fullname, email address, phone & website. + * @returns {Object} Two HTMLElement: title and body. + */ +function getUserInfoTemplate(user) { + const userAddress = `${user.address.suite}<br />${user.address.street}<br />${user.address.zipcode} ${user.address.city}`; + + const fullName = document.createElement("h2"); + const userDetails = document.createElement("dl"); + const emailLabel = createUserInfoDt("Email"); + const email = createUserInfoDd(user.email); + const addressLabel = createUserInfoDt("Address"); + const address = createUserInfoDd(userAddress, true); + const phoneLabel = createUserInfoDt("Phone"); + const phone = createUserInfoDd(user.phone); + const websiteLabel = createUserInfoDt("Website"); + const website = createUserInfoDd(user.website); + + fullName.textContent = user.name; + fullName.classList.add("user-info__title"); + userDetails.classList.add("user-info__body"); + userDetails.append( + emailLabel, + email, + addressLabel, + address, + phoneLabel, + phone, + websiteLabel, + website + ); + + return { + title: fullName, + body: userDetails, + }; +} + +/** + * Print the info for a given user. + * @param {Integer} userId - The user id. + */ +async function printUserInfo(userId) { + const api = `https://jsonplaceholder.typicode.com/users/${userId}`; + const user = await fetchData(api).then((data) => data); + const userInfo = document.querySelector(".user-info"); + const userInfoTemplate = getUserInfoTemplate(user); + userInfo.hasChildNodes() + ? userInfo.replaceChildren(userInfoTemplate.title, userInfoTemplate.body) + : userInfo.append(userInfoTemplate.title, userInfoTemplate.body); +} + +/** + * Add a class "active" to a link. + * @param {Integer} linkId - The link id. + */ +function addActiveClassTo(linkId) { + const link = document.getElementById(linkId); + link.classList.add("active"); +} + +/** + * Print the user info on click. + * @param {MouseEvent} e - The click event. + * @param {NodeList} links - The users links. + */ +function handleClick(e, links) { + e.preventDefault(); + const userId = e.target.id.split("user-")[1]; + const linkId = `user-${userId}`; + history.pushState({ userId }, e.target.textContent, e.target.href); + printUserInfo(userId); + + for (let i = 0; i < links.length; i++) { + links[i].classList.remove("active"); + } + + addActiveClassTo(linkId); +} + +/** + * Listen all users links. + */ +function listenUsersLinks() { + const usersLinks = document.querySelectorAll(".users-list__link"); + for (let i = 0; i < usersLinks.length; i++) { + const link = usersLinks[i]; + link.addEventListener("click", (e) => handleClick(e, usersLinks)); + } +} + +/** + * Load user info when URL refers to an user page. + */ +function listenURL() { + const currentURL = window.location.href; + const isUserPage = currentURL.includes("user="); + if (isUserPage) { + const userId = currentURL.split("?user=")[1]; + const linkId = `user-${userId}`; + printUserInfo(userId); + addActiveClassTo(linkId); + } +} + +/** + * Init the app. + */ +async function init() { + const api = "https://jsonplaceholder.typicode.com/users"; + const users = await fetchData(api).then((data) => data); + printUsersList(users); + listenUsersLinks(); + listenURL(); +} + +init(); diff --git a/public/projects/js-small-apps/users-list/index.html b/public/projects/js-small-apps/users-list/index.html new file mode 100644 index 0000000..6472ffe --- /dev/null +++ b/public/projects/js-small-apps/users-list/index.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>User list</title> + <link rel="stylesheet" href="style.css" /> + </head> + <body> + <header class="header"> + <h1 class="branding">Users list</h1> + </header> + <main class="main"> + <div class="users"> + <h2 class="users__title">Users</h2> + <p class="users__instructions"> + Select an user to see its personal details. + </p> + <ul class="users-list"></ul> + </div> + <div class="user-info"></div> + </main> + <footer class="footer"> + <p class="copyright">Users list. MIT 2021. Armand Philippot.</p> + </footer> + <script src="app.js"></script> + </body> +</html> diff --git a/public/projects/js-small-apps/users-list/style.css b/public/projects/js-small-apps/users-list/style.css new file mode 100644 index 0000000..4923c6f --- /dev/null +++ b/public/projects/js-small-apps/users-list/style.css @@ -0,0 +1,104 @@ +*, +*::after, +*::before { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background: #fff; + color: #000; + font-family: Arial, Helvetica, sans-serif; + font-size: 16px; + line-height: 1.618; + display: flex; + flex-flow: column; + min-height: 100vh; +} + +.header, +.main, +.footer { + width: min(calc(100vw - 2rem), 100ch); + margin-left: auto; + margin-right: auto; +} + +.header, +.footer { + padding: 1rem 0; + text-align: center; +} + +.branding { + color: hsl(219, 64%, 35%); +} + +.main { + flex: 1; + display: flex; + flex-flow: row wrap; + gap: 1rem; + margin: 2rem auto; +} + +.users, +.user-info { + border: 2px solid hsl(219, 64%, 35%); +} + +.users { + flex: 1 1 35ch; + padding: 1rem 0; +} + +.users__title, +.users__instructions { + padding: 0 1rem; +} + +.users-list { + list-style-type: none; + margin-top: 1rem; +} + +.users-list__link { + display: block; + padding: 0.2rem 1rem; + color: hsl(219, 64%, 35%); +} + +.users-list__link:hover, +.users-list__link:focus { + background: hsl(219, 64%, 35%); + color: #fff; +} + +.users-list__link:active { + text-decoration: none; +} + +.users-list__link.active { + background: hsl(219, 64%, 25%); + color: #fff; + text-decoration: none; +} + +.user-info { + flex: 1 1 60ch; + padding: 1rem; +} + +.user-info__title { + margin-bottom: 1rem; +} + +.user-info__label { + font-weight: 600; + margin-top: 1rem; +} + +.copyright { + font-size: 0.9rem; +} |
