aboutsummaryrefslogtreecommitdiffstats
path: root/public/projects/js-small-apps/users-list
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-02-20 16:11:50 +0100
committerArmand Philippot <git@armandphilippot.com>2022-02-20 16:15:08 +0100
commit73a5c7fae9ffbe9ada721148c8c454a643aceebe (patch)
treec8fad013ed9b5dd589add87f8d45cf02bbfc6e91 /public/projects/js-small-apps/users-list
parentb01239fbdcc5bbc5921f73ec0e8fee7bedd5c8e8 (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.md18
-rw-r--r--public/projects/js-small-apps/users-list/app.js187
-rw-r--r--public/projects/js-small-apps/users-list/index.html29
-rw-r--r--public/projects/js-small-apps/users-list/style.css104
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;
+}