diff --git a/client/i18n/locales/english/intro.json b/client/i18n/locales/english/intro.json index 8c7a5562bd8ec7..3fcc988b9036f4 100644 --- a/client/i18n/locales/english/intro.json +++ b/client/i18n/locales/english/intro.json @@ -3254,7 +3254,12 @@ ] }, "alda": { "title": "253", "intro": [] }, - "cvaf": { "title": "254", "intro": [] }, + "lab-fcc-forum-leaderboard": { + "title": "Build an fCC Forum Leaderboard", + "intro": [ + "For this lab you will practice asynchronous JavaScript by coding your own freeCodeCamp forum leaderboard." + ] + }, "review-asynchronous-javascript": { "title": "Asynchronous JavaScript Review", "intro": [ diff --git a/client/src/pages/learn/full-stack-developer/lab-fcc-forum-leaderboard/index.md b/client/src/pages/learn/full-stack-developer/lab-fcc-forum-leaderboard/index.md new file mode 100644 index 00000000000000..d3a4322dd81f32 --- /dev/null +++ b/client/src/pages/learn/full-stack-developer/lab-fcc-forum-leaderboard/index.md @@ -0,0 +1,9 @@ +--- +title: Introduction to the Build an fCC Forum Leaderboard +block: lab-fcc-forum-leaderboard +superBlock: full-stack-developer +--- + +## Introduction to the Build an fCC Forum Leaderboard + +For this lab you will practice asynchronous JavaScript by coding your own freeCodeCamp forum leaderboard. diff --git a/curriculum/challenges/_meta/lab-fcc-forum-leaderboard/meta.json b/curriculum/challenges/_meta/lab-fcc-forum-leaderboard/meta.json new file mode 100644 index 00000000000000..9c0b1433f66638 --- /dev/null +++ b/curriculum/challenges/_meta/lab-fcc-forum-leaderboard/meta.json @@ -0,0 +1,11 @@ +{ + "name": "Build an fCC Forum Leaderboard", + "isUpcomingChange": true, + "usesMultifileEditor": true, + "blockType": "lab", + "blockLayout": "link", + "dashedName": "lab-fcc-forum-leaderboard", + "superBlock": "full-stack-developer", + "challengeOrder": [{ "id": "673c91f0b934834bc4a3ecc2", "title": "Build an fCC Forum Leaderboard" }], + "helpCategory": "JavaScript" +} diff --git a/curriculum/challenges/english/25-front-end-development/lab-fcc-forum-leaderboard/673c91f0b934834bc4a3ecc2.md b/curriculum/challenges/english/25-front-end-development/lab-fcc-forum-leaderboard/673c91f0b934834bc4a3ecc2.md new file mode 100644 index 00000000000000..6e2361539cc107 --- /dev/null +++ b/curriculum/challenges/english/25-front-end-development/lab-fcc-forum-leaderboard/673c91f0b934834bc4a3ecc2.md @@ -0,0 +1,1422 @@ +--- +id: 673c91f0b934834bc4a3ecc2 +title: Build an fCC Forum Leaderboard +challengeType: 14 +dashedName: build-an-fcc-forum-leaderboard +demoType: onClick +--- + +# --description-- + +In this lab, you will build a freeCodeCamp forum leaderboard that displays the latest topics, users, and replies from the [freeCodeCamp forum](https://forum.freecodecamp.org/). The HTML, CSS and part of the JS have been provided for you. Feel free to explore them. + +Fulfill the user stories below and get all the tests to pass to complete the lab. + +**User Stories:** + +1. You should have a function named `timeAgo` that takes a timestamp in the ISO 8601 format as the argument. +1. The `timeAgo` function should compute the time difference between the time passed as an argument and the current time and return: + - `xm ago` (`x` represents minutes) if the amount of minutes that have passed is less than `60`. + - `xh ago` (`x` represents hours) if the amount of hours that have passed is less than `24`. + - `xd ago` (`x` represents days) otherwise. +1. You should have a function named `viewCount` that takes the number of views of a post as the argument. +1. If the value of the views passed as the argument is greater than or equal to `1000`, the `viewCount` function should return a string with the views value divided by `1000`, rounded down to the nearest whole number and the letter `k` appended to it. Otherwise, it should return the views value. +1. You should have a function named `forumCategory` that takes the id of a selected category as the argument. +1. The `forumCategory` function should verify that the selected category id is a property of the `allCategories` object and should return a string containing an anchor element with: + - the text of the `category` key of the selected category. + - a class of `category` followed by the `className` property of the selected category. + - an `href` with the value of `//`, where `` is the `className` property of the selected category and `id` is the argument passed to `forumCategory`. +1. If the `allCategories` object does not have the selected category id as its property, `className` and `category` should be indicated as `general`. +1. You should have a function named `avatars` that takes two arrays representing posters and users, respectively. +1. The `avatars` function should return a string made by joining `img` elements, one for each poster found inside the user array. *Hint:* You can find users by comparing the `user_id` property of the poster with the `id` property` of the user. +1. The `avatars` function should set each avatar's size by accessing the `avatar_template` property and replacing `{size}` with `30`. +1. Each image element should have an alt text with the value of the `name` property of the poster. +1. Each image element should have a source with the value of the `avatar_template` property of the poster. In case `avatar_template` contains a relative path, you should set the source to `/`. +1. You should have a function named `showLatestPosts` that takes a single parameter. +1. The `showLatestPosts` should extract the `users` and `topic_list` properties from the object passed as argument. Also, it should process the following properties of the `topics` object contained in `topic_list`: + - `id`: the id of the post + - `title`: the title of the post + - `views`: the number of views of the post + - `posts_count`: the number of replies to the topic + - `slug`: the slug of the post + - `posters`: the posters for that topic + - `category_id`: an integer indicating the category id for the post + - `bumped_at`: a timestamp in the ISO 8601 format +1. The `showLatestPosts` should set the inner HTML of `#posts-container` to a string made by joining `tr` elements, one for each item in `topics`. +1. Each `tr` element should have five `td` elements in it: + - a `td` containing two anchor elements, one with the class of `post-title`, an `href` of `//`, an anchor text of ``, and one obtained by calling `forumCategory` with `category_id`. + - a `td` containing the images returned by the `avatars` function called with `posters` and `users` as arguments and nested within a `div` element with the class of `avatar-container`. + - a `td` containing the number of replies to the post. *Hint:* use `posts_count - 1`. + - a `td` containing the number of views of the post. + - a `td` containing the time passed since the last activity. +1. You should have an async function named `fetchData`. +1. The `fetchData` function should request data from `forumLatest` and call `showLatestPosts` passing it the response parsed as JSON. +1. If there's an error when fetching data, the `fetchData` function should log the error to the console. + +# --hints-- + +You should have a function named `timeAgo` that takes a single argument. + +```js +assert.isFunction(timeAgo); +assert.lengthOf(timeAgo, 1); +``` + +When the time difference between the time passed as argument and the current time is `50` minutes, `timeAgo` should return `50m ago` but found `--fcc-actual--`. + +```js +const generateTime = () => { + const currentTime = new Date(); + return new Date(currentTime - (1000 * 60 * 50)).toISOString() +} +const expected = "50m ago"; +const actual = timeAgo(generateTime()); +assert.equal(expected, actual); +``` + +When the time difference between the time passed as argument and the current time is `60` minutes, `timeAgo` should return `1h ago` but found `--fcc-actual--`. + +```js +const generateTime = () => { + const currentTime = new Date(); + return new Date(currentTime - (1000 * 60 * 60)).toISOString() +} +const expected = "1h ago"; +const actual = timeAgo(generateTime()); +assert.equal(expected, actual); +``` + +When the time difference between the time passed as argument and the current time is `15` hours, `timeAgo` should return `15h ago` but found `--fcc-actual--`. + +```js +const generateTime = () => { + const currentTime = new Date(); + return new Date(currentTime - (1000 * 60 * 60 * 15)).toISOString() +} +const expected = "15h ago"; +const actual = timeAgo(generateTime()); +assert.equal(expected, actual); +``` + +When the time difference between the time passed as argument and the current time is `24` hours, `timeAgo` should return `1d ago` but found `--fcc-actual--`. + +```js +const generateTime = () => { + const currentTime = new Date(); + return new Date(currentTime - (1000 * 60 * 60 * 24)).toISOString() +} +const expected = "1d ago"; +const actual = timeAgo(generateTime()); +assert.equal(expected, actual); +``` + +When the time difference between the time passed as argument and the current time is `3` days, `timeAgo` should return `3d ago` but found `--fcc-actual--`. + +```js +const generateTime = () => { + const currentTime = new Date(); + return new Date(currentTime - (1000 * 60 * 60 * 24 * 3)).toISOString() +} +const expected = "3d ago"; +const actual = timeAgo(generateTime()); +assert.equal(expected, actual); +``` + +You should have a function named `viewCount` that takes a single argument. + +```js +assert.isFunction(viewCount); +assert.lengthOf(viewCount, 1); +``` + +`viewCount(597)` should return `597`. + +```js +assert.strictEqual(597, viewCount(597)); +``` + +`viewCount(1000)` should return `1k`. + +```js +assert.equal("1k", viewCount(1000)); +``` + +`viewCount(2730)` should return `2k`. + +```js +assert.equal("2k", viewCount(2730)); +``` + +You should have a function named `forumCategory` that takes a single argument. + +```js +assert.isFunction(forumCategory); +assert.lengthOf(forumCategory, 1); +``` + +`forumCategory(299)` should return a string containing an anchor element with the text `Career Advice`. + +```js +let actual = forumCategory(299); +assert.match(actual, /^<\s*a.+?>\s*Career Advice\s*<\/a>$/); +// prevent hardcoding + +actual = forumCategory(409); +assert.match(actual, /^<\s*a.+?>\s*Project Feedback\s*<\/a>$/); +``` + +`forumCategory(299)` should return a string containing an anchor element with `href="https://forum.freecodecamp.org/c/career/299"`. + +```js +let actual = forumCategory(299); +assert.match(actual, /href=("|')https:\/\/forum\.freecodecamp\.org\/c\/career\/299\1/); + +// prevent hardcoding +actual = forumCategory(409); +assert.match(actual, /href=("|')https:\/\/forum\.freecodecamp\.org\/c\/feedback\/409\1/); +``` + +`forumCategory(299)` should return a string containing an anchor element with `class="category career"`. + +```js +let actual = forumCategory(299); +assert.match(actual, /class=("|')category\s+career\1/); + +// prevent hardcoding +actual = forumCategory(409); +assert.match(actual, /class=("|')category\s+feedback\1/); +``` + +`forumCategory(200)` should return a string containing an anchor element with the text `General`. + +```js +const actual = forumCategory(200); +assert.match(actual, /^<\s*a.+?>\s*General\s*<\/a>$/); +``` + +`forumCategory(200)` should return a string containing an anchor element with `href="https://forum.freecodecamp.org/c/general/200"`. + +```js +let actual = forumCategory(200); +assert.match(actual, /href=("|')https:\/\/forum\.freecodecamp\.org\/c\/general\/200/); + +actual = forumCategory(220); +assert.match(actual, /href=("|')https:\/\/forum\.freecodecamp\.org\/c\/general\/220/); +``` + +`forumCategory(200)` should return a string containing an anchor element with `class="category career"`. + +```js +const actual = forumCategory(200); +assert.match(actual, /class=("|')category\s+general\1/); +``` + +You should have a function named `avatars` that takes two arguments. + +```js +assert.isFunction(avatars); +assert.lengthOf(avatars, 2); +``` + +The `avatars` function should return a string made by joining `img` elements, one for each poster found in the user array. + +```js +const posters = [{ "user_id": 6 }, { "user_id": 285941 }, { "user_id": 170865 }] +const users = [ + { + avatar_template: "/user_avatar/forum.freecodecamp.org/quincylarson/{size}/212400_2.png", + id: 6, + name: "Quincy Larson", + username: "QuincyLarson" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/jwilkins.oboe/{size}/179497_2.png", + id: 285941, + name: "Jessica Wilkins", + username: "jwilkins.oboe" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/ilenia/{size}/270648_2.png", + id: 170865, + name: "Ilenia", + username: "ilenia" + }, + {id: 20} +] +const actual = avatars(posters, users); +const matches = actual.match(/<\s*img\s+.+?>/g); +assert.lengthOf(matches, 3) +``` + +Each `img` element in the string returned by the `avatars` function should have an `alt` text with the value of the `name` property of the poster. + +```js +const posters = [{ "user_id": 6 }, { "user_id": 285941 }, { "user_id": 170865 }] +const users = [ + { + avatar_template: "/user_avatar/forum.freecodecamp.org/quincylarson/{size}/212400_2.png", + id: 6, + name: "Quincy Larson", + username: "QuincyLarson" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/jwilkins.oboe/{size}/179497_2.png", + id: 285941, + name: "Jessica Wilkins", + username: "jwilkins.oboe" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/ilenia/{size}/270648_2.png", + id: 170865, + name: "Ilenia", + username: "ilenia" + }, + {id: 20} +] +const actual = avatars(posters, users); +const matches = actual.match(/<\s*img\s+.+?>/g); + +assert.match(matches[0], /alt="Quincy Larson"/); +assert.match(matches[1], /alt="Jessica Wilkins"/); +assert.match(matches[2], /alt="Ilenia"/); +``` + +The `avatars` function should set each avatar's size by accessing the `avatar_template` property and replacing `{size}` with `30`. + +```js +const posters = [{ "user_id": 6 }, { "user_id": 285941 }, { "user_id": 170865 }] +const users = [ + { + avatar_template: "/user_avatar/forum.freecodecamp.org/quincylarson/{size}/212400_2.png", + id: 6, + name: "Quincy Larson", + username: "QuincyLarson" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/jwilkins.oboe/{size}/179497_2.png", + id: 285941, + name: "Jessica Wilkins", + username: "jwilkins.oboe" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/ilenia/{size}/270648_2.png", + id: 170865, + name: "Ilenia", + username: "ilenia" + }, + {id: 20} +] +const actual = avatars(posters, users); +assert.notMatch(actual, /\{size\}/); +assert.lengthOf(actual.match(/\/30\//g), 3); +``` + +Each `img` element in the string returned by the `avatars` function should have the `src` with the value of the `avatar_template` property of the poster. In case `avatar_template` contains a relative path, it should be set to `<avatarUrl>/<avatar_template>`. + +```js +const posters = [{ "user_id": 6 }, { "user_id": 285941 }, { "user_id": 170865 }] +const users = [ + { + avatar_template: "/user_avatar/forum.freecodecamp.org/quincylarson/{size}/212400_2.png", + id: 6, + name: "Quincy Larson", + username: "QuincyLarson" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/jwilkins.oboe/{size}/179497_2.png", + id: 285941, + name: "Jessica Wilkins", + username: "jwilkins.oboe" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/ilenia/{size}/270648_2.png", + id: 170865, + name: "Ilenia", + username: "ilenia" + }, + {id: 20} +] +const actual = avatars(posters, users); + +const matches = actual.match(/<\s*img\s+.+?>/g); + +assert.match(matches[0], /src="https:\/\/sea1\.discourse-cdn\.com\/freecodecamp\/user_avatar\/forum\.freecodecamp\.org\/quincylarson\/30\/212400_2\.png"/); +assert.match(matches[1], /src="https:\/\/sea1\.discourse-cdn\.com\/freecodecamp\/user_avatar\/forum\.freecodecamp\.org\/jwilkins\.oboe\/30\/179497_2\.png"/); +assert.match(matches[2], /src="https:\/\/sea1\.discourse-cdn\.com\/freecodecamp\/user_avatar\/forum\.freecodecamp\.org\/ilenia\/30\/270648_2\.png"/); +``` + +You should have a function named `showLatestPosts` that takes a single parameter. + +```js +assert.isFunction(showLatestPosts); +assert.lengthOf(showLatestPosts, 1); +``` + +You should have a function named `fetchData`. + +```js +assert.isFunction(fetchData); +``` + +Your `fetchData` function should request data from `https://cdn.freecodecamp.org/curriculum/forum-latest/latest.json`. + +```js +const testArr = []; +const temp = fetch; +try { + fetch = source => { + testArr.push(source); + return temp(source); + } + fetchData(); + assert.deepEqual(testArr, ["https://cdn.freecodecamp.org/curriculum/forum-latest/latest.json"]) +} finally { + fetch = temp; +} +``` + +Your `fetchData` function should call `showLatestPosts`. + +```js +const testArr = []; +const temp = showLatestPosts; +async () => { + try { + showLatestPosts = data => { + testArr.push(data); + return temp(data); + } + await fetchData(); + assert.isNotEmpty(testArr); + } catch (err) { + throw new Error(err); + } finally { + fetch = temp; + } +} +``` + +If there is an error, your `fetchData` function should log the error to the console. + +```js +const testArr = []; +const temp1 = fetch; +const temp2 = console.log; +async () => { + try { + console.log = obj => {testArr.push(obj.toString())}; + fetch = source => {throw new Error("This is a test error");} + await fetchData(); + assert.deepEqual(testArr, ["Error: This is a test error"]); + } finally { + fetch = temp1; + console.log = temp2; + } +} +``` + +`showLatestPosts` should set the inner HTML of `#posts-container` to a string made by joining `tr` elements, one for each item in `topics`. + +```js +const data = { + users: [ + { + avatar_template: "/user_avatar/forum.freecodecamp.org/quincylarson/{size}/212400_2.png", + id: 6, + name: "Quincy Larson", + username: "QuincyLarson" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/jwilkins.oboe/{size}/179497_2.png", + id: 285941, + name: "Jessica Wilkins", + username: "jwilkins.oboe" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/ilenia/{size}/270648_2.png", + id: 170865, + name: "Ilenia", + username: "ilenia" + } + ], + topic_list: { + topics: [ + { + bumped_at: "2024-04-15T16:01:26.403Z", + category_id: 1, + id: 684569, + posters: [{user_id: 6}, {user_id: 170865}, {user_id: 285941}], + posts_count: 8, + slug: "the-freecodecamp-podcast-is-back-now-with-video", + title: "The freeCodeCamp Podcast is back – now with video", + views: 542 + }, + { + bumped_at: "2024-04-19T13:52:03.523Z", + category_id: 421, + id: 686149, + posters: [{user_id: 170865}], + posts_count: 1, + slug: "problem-with-making-changes-to-styles-js", + title: "Problem with making changes to styles. (JS)", + views: 9 + } + ] + } +} +const pContainer = document.getElementById("posts-container"); +pContainer.innerHTML = ""; +showLatestPosts(data); +assert.lengthOf(pContainer.querySelectorAll("tr"), 2); +``` + +Each `tr` element from the string returned by `showLatestPosts` should contain `5` `td` elements. + +```js +const data = { + users: [ + { + avatar_template: "/user_avatar/forum.freecodecamp.org/quincylarson/{size}/212400_2.png", + id: 6, + name: "Quincy Larson", + username: "QuincyLarson" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/jwilkins.oboe/{size}/179497_2.png", + id: 285941, + name: "Jessica Wilkins", + username: "jwilkins.oboe" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/ilenia/{size}/270648_2.png", + id: 170865, + name: "Ilenia", + username: "ilenia" + } + ], + topic_list: { + topics: [ + { + bumped_at: "2024-04-15T16:01:26.403Z", + category_id: 1, + id: 684569, + posters: [{user_id: 6}, {user_id: 170865}, {user_id: 285941}], + posts_count: 8, + slug: "the-freecodecamp-podcast-is-back-now-with-video", + title: "The freeCodeCamp Podcast is back – now with video", + views: 542 + }, + { + bumped_at: "2024-04-19T13:52:03.523Z", + category_id: 421, + id: 686149, + posters: [{user_id: 170865}], + posts_count: 1, + slug: "problem-with-making-changes-to-styles-js", + title: "Problem with making changes to styles. (JS)", + views: 9 + } + ] + } +} +const pContainer = document.getElementById("posts-container"); +pContainer.innerHTML = ""; +showLatestPosts(data); +assert.lengthOf(pContainer.querySelectorAll("tr:first-child>td"), 5); +assert.lengthOf(pContainer.querySelectorAll("tr:last-child>td"), 5); +``` + +The first `td` element of each table row from the string returned by `showLatestPosts` should contain two anchor elements, the first with the class of `post-title`, an `href` of `<forumTopicUrl>/<slug>/<id>`, an anchor text of `<title>`, and the second obtained by calling `forumCategory` with `category_id`. + +```js +const data = { + users: [ + { + avatar_template: "/user_avatar/forum.freecodecamp.org/quincylarson/{size}/212400_2.png", + id: 6, + name: "Quincy Larson", + username: "QuincyLarson" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/jwilkins.oboe/{size}/179497_2.png", + id: 285941, + name: "Jessica Wilkins", + username: "jwilkins.oboe" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/ilenia/{size}/270648_2.png", + id: 170865, + name: "Ilenia", + username: "ilenia" + } + ], + topic_list: { + topics: [ + { + bumped_at: "2024-04-15T16:01:26.403Z", + category_id: 1, + id: 684569, + posters: [{user_id: 6}, {user_id: 170865}, {user_id: 285941}], + posts_count: 8, + slug: "the-freecodecamp-podcast-is-back-now-with-video", + title: "The freeCodeCamp Podcast is back – now with video", + views: 542 + }, + { + bumped_at: "2024-04-19T13:52:03.523Z", + category_id: 421, + id: 686149, + posters: [{user_id: 170865}], + posts_count: 1, + slug: "problem-with-making-changes-to-styles-js", + title: "Problem with making changes to styles. (JS)", + views: 9 + } + ] + } +} +const pContainer = document.getElementById("posts-container"); +pContainer.innerHTML = ""; +showLatestPosts(data); +const anchors1 = pContainer.querySelectorAll("tr:first-child>td>a"); +assert.lengthOf(anchors1, 2); + +const anchors2 = pContainer.querySelectorAll("tr:last-child>td>a"); +assert.lengthOf(anchors2, 2); + +assert.equal(anchors1[0].classList[0], "post-title"); +assert.equal(anchors1[0].href, "https://forum.freecodecamp.org/t/the-freecodecamp-podcast-is-back-now-with-video/684569"); +assert.equal(anchors1[0].innerText.trim(), "The freeCodeCamp Podcast is back – now with video"); + +assert.equal(anchors1[1].classList[0], "category"); +assert.equal(anchors1[1].classList[1], "general"); +assert.equal(anchors1[1].href, "https://forum.freecodecamp.org/c/general/1"); + + +assert.equal(anchors2[0].classList[0], "post-title"); +assert.equal(anchors2[0].href, "https://forum.freecodecamp.org/t/problem-with-making-changes-to-styles-js/686149"); +assert.equal(anchors2[0].innerText.trim(), "Problem with making changes to styles. (JS)"); + +assert.equal(anchors2[1].classList[0], "category"); +assert.equal(anchors2[1].classList[1], "javascript"); +assert.equal(anchors2[1].href, "https://forum.freecodecamp.org/c/javascript/421"); +``` + +The second `td` element of each table row from the string returned by `showLatestPosts` should contain the images returned by the `avatars` function called with `posters` and `users` as arguments, nested within a `div` element with the class of `avatar-container`. + +```js +const data = { + users: [ + { + avatar_template: "/user_avatar/forum.freecodecamp.org/quincylarson/{size}/212400_2.png", + id: 6, + name: "Quincy Larson", + username: "QuincyLarson" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/jwilkins.oboe/{size}/179497_2.png", + id: 285941, + name: "Jessica Wilkins", + username: "jwilkins.oboe" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/ilenia/{size}/270648_2.png", + id: 170865, + name: "Ilenia", + username: "ilenia" + } + ], + topic_list: { + topics: [ + { + bumped_at: "2024-04-15T16:01:26.403Z", + category_id: 1, + id: 684569, + posters: [{user_id: 6}, {user_id: 170865}, {user_id: 285941}], + posts_count: 8, + slug: "the-freecodecamp-podcast-is-back-now-with-video", + title: "The freeCodeCamp Podcast is back – now with video", + views: 542 + }, + { + bumped_at: "2024-04-19T13:52:03.523Z", + category_id: 421, + id: 686149, + posters: [{user_id: 170865}], + posts_count: 1, + slug: "problem-with-making-changes-to-styles-js", + title: "Problem with making changes to styles. (JS)", + views: 9 + } + ] + } +} +const pContainer = document.getElementById("posts-container"); +pContainer.innerHTML = ""; +showLatestPosts(data); + +const div1 = pContainer.querySelector("tr:first-child>td:nth-child(2)>div"); +assert.equal(div1.classList[0], "avatar-container"); + +const div2 = pContainer.querySelector("tr:last-child>td:nth-child(2)>div"); +assert.equal(div2.classList[0], "avatar-container"); + +const imgs1 = div1.querySelectorAll("img"); +assert.lengthOf(imgs1, 3); +assert.equal(imgs1[0].src, "https://sea1.discourse-cdn.com/freecodecamp/user_avatar/forum.freecodecamp.org/quincylarson/30/212400_2.png"); +assert.equal(imgs1[0].alt, "Quincy Larson"); +assert.equal(imgs1[1].src, "https://sea1.discourse-cdn.com/freecodecamp/user_avatar/forum.freecodecamp.org/ilenia/30/270648_2.png"); +assert.equal(imgs1[1].alt, "Ilenia"); +assert.equal(imgs1[2].src, "https://sea1.discourse-cdn.com/freecodecamp/user_avatar/forum.freecodecamp.org/jwilkins.oboe/30/179497_2.png"); +assert.equal(imgs1[2].alt, "Jessica Wilkins"); + +const imgs2 = div2.querySelectorAll("img"); +assert.lengthOf(imgs2, 1); +assert.equal(imgs2[0].src, "https://sea1.discourse-cdn.com/freecodecamp/user_avatar/forum.freecodecamp.org/ilenia/30/270648_2.png"); +assert.equal(imgs2[0].alt, "Ilenia"); +``` + +The third `td` element of each table row from the string returned by `showLatestPosts` should contain the number of replies to the post. *Hint:* use `posts_count - 1`. + +```js +const data = { + users: [ + { + avatar_template: "/user_avatar/forum.freecodecamp.org/quincylarson/{size}/212400_2.png", + id: 6, + name: "Quincy Larson", + username: "QuincyLarson" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/jwilkins.oboe/{size}/179497_2.png", + id: 285941, + name: "Jessica Wilkins", + username: "jwilkins.oboe" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/ilenia/{size}/270648_2.png", + id: 170865, + name: "Ilenia", + username: "ilenia" + } + ], + topic_list: { + topics: [ + { + bumped_at: "2024-04-15T16:01:26.403Z", + category_id: 1, + id: 684569, + posters: [{user_id: 6}, {user_id: 170865}, {user_id: 285941}], + posts_count: 8, + slug: "the-freecodecamp-podcast-is-back-now-with-video", + title: "The freeCodeCamp Podcast is back – now with video", + views: 542 + }, + { + bumped_at: "2024-04-19T13:52:03.523Z", + category_id: 421, + id: 686149, + posters: [{user_id: 170865}], + posts_count: 1, + slug: "problem-with-making-changes-to-styles-js", + title: "Problem with making changes to styles. (JS)", + views: 9 + } + ] + } +} +const pContainer = document.getElementById("posts-container"); +pContainer.innerHTML = ""; +showLatestPosts(data); + +assert.equal(pContainer.querySelector("tr:first-child>td:nth-child(3)").innerText, "7"); + +assert.equal(pContainer.querySelector("tr:last-child>td:nth-child(3)").innerText, "0"); +``` + +The fourth `td` element of each table row from the string returned by `showLatestPosts` should contain the number of views of the post. + +```js +const data = { + users: [ + { + avatar_template: "/user_avatar/forum.freecodecamp.org/quincylarson/{size}/212400_2.png", + id: 6, + name: "Quincy Larson", + username: "QuincyLarson" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/jwilkins.oboe/{size}/179497_2.png", + id: 285941, + name: "Jessica Wilkins", + username: "jwilkins.oboe" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/ilenia/{size}/270648_2.png", + id: 170865, + name: "Ilenia", + username: "ilenia" + } + ], + topic_list: { + topics: [ + { + bumped_at: "2024-04-15T16:01:26.403Z", + category_id: 1, + id: 684569, + posters: [{user_id: 6}, {user_id: 170865}, {user_id: 285941}], + posts_count: 8, + slug: "the-freecodecamp-podcast-is-back-now-with-video", + title: "The freeCodeCamp Podcast is back – now with video", + views: 542 + }, + { + bumped_at: "2024-04-19T13:52:03.523Z", + category_id: 421, + id: 686149, + posters: [{user_id: 170865}], + posts_count: 1, + slug: "problem-with-making-changes-to-styles-js", + title: "Problem with making changes to styles. (JS)", + views: 9 + } + ] + } +} +const pContainer = document.getElementById("posts-container"); +pContainer.innerHTML = ""; +showLatestPosts(data); + +assert.equal(pContainer.querySelector("tr:first-child>td:nth-child(4)").innerText, "542"); + +assert.equal(pContainer.querySelector("tr:last-child>td:nth-child(4)").innerText, "9"); +``` + +The fifth `td` element of each table row from the string returned by `showLatestPosts` should contain time passed since the last activity, generated using the `timeAgo` function. + +```js +const data = { + users: [ + { + avatar_template: "/user_avatar/forum.freecodecamp.org/quincylarson/{size}/212400_2.png", + id: 6, + name: "Quincy Larson", + username: "QuincyLarson" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/jwilkins.oboe/{size}/179497_2.png", + id: 285941, + name: "Jessica Wilkins", + username: "jwilkins.oboe" + }, + { + avatar_template: "/user_avatar/forum.freecodecamp.org/ilenia/{size}/270648_2.png", + id: 170865, + name: "Ilenia", + username: "ilenia" + } + ], + topic_list: { + topics: [ + { + bumped_at: "2024-04-15T16:01:26.403Z", + category_id: 1, + id: 684569, + posters: [{user_id: 6}, {user_id: 170865}, {user_id: 285941}], + posts_count: 8, + slug: "the-freecodecamp-podcast-is-back-now-with-video", + title: "The freeCodeCamp Podcast is back – now with video", + views: 542 + }, + { + bumped_at: "2024-04-19T13:52:03.523Z", + category_id: 421, + id: 686149, + posters: [{user_id: 170865}], + posts_count: 1, + slug: "problem-with-making-changes-to-styles-js", + title: "Problem with making changes to styles. (JS)", + views: 9 + } + ] + } +} +const calcTime = (time) => { + const currentTime = new Date(); + const lastPost = new Date(time); + const timeDifference = currentTime - lastPost; + const msPerMinute = 1000 * 60; + const minutesAgo = Math.floor(timeDifference / msPerMinute); + const hoursAgo = Math.floor(minutesAgo / 60); + const daysAgo = Math.floor(hoursAgo / 24); + return `${daysAgo}d ago`; +}; +const pContainer = document.getElementById("posts-container"); +pContainer.innerHTML = ""; +showLatestPosts(data); + +assert.equal(pContainer.querySelector("tr:first-child>td:nth-child(5)").innerText, calcTime("2024-04-15T16:01:26.403Z")); + +assert.equal(pContainer.querySelector("tr:last-child>td:nth-child(5)").innerText, calcTime("2024-04-19T13:52:03.523Z")); +``` + +# --seed-- + +## --seed-contents-- + +```html +<!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>fCC Forum Leaderboard + + + +
+ +

Latest Topics

+
+
+
+ + + + + + + + + + + +
TopicsAvatarsRepliesViewsActivity
+
+
+ + + +``` + +```css +* { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + :root { + --main-bg-color: #2a2a40; + --black: #000; + --dark-navy: #0a0a23; + --dark-grey: #d0d0d5; + --medium-grey: #dfdfe2; + --light-grey: #f5f6f7; + --peach: #f28373; + --salmon-color: #f0aea9; + --light-blue: #8bd9f6; + --light-orange: #f8b172; + --light-green: #93cb5b; + --golden-yellow: #f1ba33; + --gold: #f9aa23; + --green: #6bca6b; + } + + body { + background-color: var(--main-bg-color); + } + + nav { + background-color: var(--dark-navy); + padding: 10px 0; + } + + .fcc-logo { + width: 210px; + display: block; + margin: auto; + } + + .title { + margin: 25px 0; + text-align: center; + color: var(--light-grey); + } + + .table-wrapper { + padding: 0 25px; + overflow-x: auto; + } + + table { + width: 100%; + color: var(--dark-grey); + margin: auto; + table-layout: fixed; + border-collapse: collapse; + overflow-x: scroll; + } + + #topics { + text-align: start; + width: 60%; + } + + th { + border-bottom: 2px solid var(--dark-grey); + padding-bottom: 10px; + font-size: 1.3rem; + } + + td:not(:first-child) { + text-align: center; + } + + td { + border-bottom: 1px solid var(--dark-grey); + padding: 20px 0; + } + + .post-title { + font-size: 1.2rem; + color: var(--medium-grey); + text-decoration: none; + } + + .category { + padding: 3px; + color: var(--black); + text-decoration: none; + display: block; + width: fit-content; + margin: 10px 0 10px; + } + + .career { + background-color: var(--salmon-color); + } + + .feedback, + .html-css { + background-color: var(--light-blue); + } + + .support { + background-color: var(--light-orange); + } + + .general { + background-color: var(--light-green); + } + + .javascript { + background-color: var(--golden-yellow); + } + + .backend { + background-color: var(--gold); + } + + .python { + background-color: var(--green); + } + + .motivation { + background-color: var(--peach); + } + + .avatar-container { + display: flex; + justify-content: center; + gap: 10px; + flex-wrap: wrap; + } + + .avatar-container img { + width: 30px; + height: 30px; + } + + @media (max-width: 750px) { + .table-wrapper { + padding: 0 15px; + } + + table { + width: 700px; + } + + th { + font-size: 1.2rem; + } + + .post-title { + font-size: 1.1rem; + } + } +``` + +```js +const forumLatest = "https://cdn.freecodecamp.org/curriculum/forum-latest/latest.json"; +const forumTopicUrl = "https://forum.freecodecamp.org/t/"; +const forumCategoryUrl = "https://forum.freecodecamp.org/c/"; +const avatarUrl = "https://sea1.discourse-cdn.com/freecodecamp"; + +const allCategories = { + 299: { category: "Career Advice", className: "career" }, + 409: { category: "Project Feedback", className: "feedback" }, + 417: { category: "freeCodeCamp Support", className: "support" }, + 421: { category: "JavaScript", className: "javascript" }, + 423: { category: "HTML - CSS", className: "html-css" }, + 424: { category: "Python", className: "python" }, + 432: { category: "You Can Do This!", className: "motivation" }, + 560: { category: "Backend Development", className: "backend" }, +}; +``` + +# --solutions-- + +```html + + + + + + + fCC Forum Leaderboard + + + +
+ +

Latest Topics

+
+
+
+ + + + + + + + + + + +
TopicsAvatarsRepliesViewsActivity
+
+
+ + + +``` + +```css +* { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + :root { + --main-bg-color: #2a2a40; + --black: #000; + --dark-navy: #0a0a23; + --dark-grey: #d0d0d5; + --medium-grey: #dfdfe2; + --light-grey: #f5f6f7; + --peach: #f28373; + --salmon-color: #f0aea9; + --light-blue: #8bd9f6; + --light-orange: #f8b172; + --light-green: #93cb5b; + --golden-yellow: #f1ba33; + --gold: #f9aa23; + --green: #6bca6b; + } + + body { + background-color: var(--main-bg-color); + } + + nav { + background-color: var(--dark-navy); + padding: 10px 0; + } + + .fcc-logo { + width: 210px; + display: block; + margin: auto; + } + + .title { + margin: 25px 0; + text-align: center; + color: var(--light-grey); + } + + .table-wrapper { + padding: 0 25px; + overflow-x: auto; + } + + table { + width: 100%; + color: var(--dark-grey); + margin: auto; + table-layout: fixed; + border-collapse: collapse; + overflow-x: scroll; + } + + #topics { + text-align: start; + width: 60%; + } + + th { + border-bottom: 2px solid var(--dark-grey); + padding-bottom: 10px; + font-size: 1.3rem; + } + + td:not(:first-child) { + text-align: center; + } + + td { + border-bottom: 1px solid var(--dark-grey); + padding: 20px 0; + } + + .post-title { + font-size: 1.2rem; + color: var(--medium-grey); + text-decoration: none; + } + + .category { + padding: 3px; + color: var(--black); + text-decoration: none; + display: block; + width: fit-content; + margin: 10px 0 10px; + } + + .career { + background-color: var(--salmon-color); + } + + .feedback, + .html-css { + background-color: var(--light-blue); + } + + .support { + background-color: var(--light-orange); + } + + .general { + background-color: var(--light-green); + } + + .javascript { + background-color: var(--golden-yellow); + } + + .backend { + background-color: var(--gold); + } + + .python { + background-color: var(--green); + } + + .motivation { + background-color: var(--peach); + } + + .avatar-container { + display: flex; + justify-content: center; + gap: 10px; + flex-wrap: wrap; + } + + .avatar-container img { + width: 30px; + height: 30px; + } + + @media (max-width: 750px) { + .table-wrapper { + padding: 0 15px; + } + + table { + width: 700px; + } + + th { + font-size: 1.2rem; + } + + .post-title { + font-size: 1.1rem; + } + } +``` + +```js +const forumLatest = "https://cdn.freecodecamp.org/curriculum/forum-latest/latest.json"; +const forumTopicUrl = "https://forum.freecodecamp.org/t/"; +const forumCategoryUrl = "https://forum.freecodecamp.org/c/"; +const avatarUrl = "https://sea1.discourse-cdn.com/freecodecamp"; + +const postsContainer = document.getElementById("posts-container"); + +const allCategories = { + 299: { category: "Career Advice", className: "career" }, + 409: { category: "Project Feedback", className: "feedback" }, + 417: { category: "freeCodeCamp Support", className: "support" }, + 421: { category: "JavaScript", className: "javascript" }, + 423: { category: "HTML - CSS", className: "html-css" }, + 424: { category: "Python", className: "python" }, + 432: { category: "You Can Do This!", className: "motivation" }, + 560: { category: "Backend Development", className: "backend" }, +}; + +const forumCategory = (id) => { + let selectedCategory = {}; + + if (allCategories.hasOwnProperty(id)) { + const { className, category } = allCategories[id]; + + selectedCategory.className = className; + selectedCategory.category = category; + } else { + selectedCategory.className = "general"; + selectedCategory.category = "General"; + selectedCategory.id = 1; + } + const url = `${forumCategoryUrl}${selectedCategory.className}/${id}`; + const linkText = selectedCategory.category; + const linkClass = `category ${selectedCategory.className}`; + + return ` + ${linkText} + `; +}; + +const timeAgo = (time) => { + const currentTime = new Date(); + const lastPost = new Date(time); + + const timeDifference = currentTime - lastPost; + const msPerMinute = 1000 * 60; + + const minutesAgo = Math.floor(timeDifference / msPerMinute); + const hoursAgo = Math.floor(minutesAgo / 60); + const daysAgo = Math.floor(hoursAgo / 24); + + if (minutesAgo < 60) { + return `${minutesAgo}m ago`; + } + + if (hoursAgo < 24) { + return `${hoursAgo}h ago`; + } + + return `${daysAgo}d ago`; +}; + +const viewCount = (views) => { + const thousands = Math.floor(views / 1000); + + if (views >= 1000) { + return `${thousands}k`; + } + + return views; +}; + +const avatars = (posters, users) => { + return posters + .map((poster) => { + const user = users.find((user) => user.id === poster.user_id); + if (user) { + const avatar = user.avatar_template.replace(/{size}/, 30); + const userAvatarUrl = avatar.startsWith("/user_avatar/") + ? avatarUrl.concat(avatar) + : avatar; + return `${user.name}`; + } + }) + .join(""); +}; + +const fetchData = async () => { + try { + const res = await fetch(forumLatest); + const data = await res.json(); + showLatestPosts(data); + } catch (err) { + console.log(err); + } +}; + +fetchData(); + +const showLatestPosts = (data) => { + const { topic_list, users } = data; + const { topics } = topic_list; + + postsContainer.innerHTML = topics.map((item) => { + const { + id, + title, + views, + posts_count, + slug, + posters, + category_id, + bumped_at, + } = item; + + return ` + + + + ${title} + + ${forumCategory(category_id)} + + +
+ ${avatars(posters, users)} +
+ + ${posts_count - 1} + ${viewCount(views)} + ${timeAgo(bumped_at)} + `; + }).join(""); +}; +``` diff --git a/curriculum/superblock-structure/full-stack.json b/curriculum/superblock-structure/full-stack.json index dc145443fc0e81..6aa830c35d4fc8 100644 --- a/curriculum/superblock-structure/full-stack.json +++ b/curriculum/superblock-structure/full-stack.json @@ -522,6 +522,7 @@ "blocks": [ { "dashedName": "lecture-understanding-asynchronous-programming" }, { "dashedName": "workshop-fcc-authors-page" }, + { "dashedName": "lab-fcc-forum-leaderboard" }, { "dashedName": "review-asynchronous-javascript" }, { "dashedName": "quiz-asynchronous-javascript" } ]