--- id: 673c91f0b934834bc4a3ecc2 title: Build an fCC Forum Leaderboard challengeType: 25 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. The API endpoint returns JSON in the following shape: ```json { "users": [ { "id": 6, "username": "QuincyLarson", "name": "Quincy Larson", "avatar_template": "/user_avatar/QuincyLarson_{size}.png" }, { "id": 576147, "username": "JOY-OKORO", "name": "Joy Okoro", "avatar_template": "/user_avatar/JOY-OKORO_{size}.png" } ], "topic_list": { "topics": [ { "id": 684569, "title": "The freeCodeCamp Podcast is back – now with video", "slug": "the-freecodecamp-podcast-is-back-now-with-video", "posts_count": 8, "views": 542, "bumped_at": "2024-04-15T16:01:26.403Z", "category_id": 1, "posters": [ { "user_id": 6 }, { "user_id": 576147 } ] } ] } } ``` Note that `avatar_template` values are relative paths: they start with `/` and contain a `{size}` placeholder that should be replaced with a pixel size. The `avatarUrl` constant holds the base URL to prepend when the path is relative. **Objective:** 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` formed by appending `/` to `forumCategoryUrl` (note: `forumCategoryUrl` already ends with `/`, so do not add an extra `/` separator), 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, `category` should be indicated as `General` and `className` 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 `user_id` in the `posters` array. Find the `img` URL by looking up the `user_id` property in the `posters` array and find the matching `id` property in the `users` array. 1. The `avatars` function should set each avatar's size by accessing the `avatar_template` property of the matched user object (found in the `users` array by comparing its `id` to the `user_id` in `posters`) and replacing the `{size}` placeholder in the URL string with `30`. 1. Each image element should have an alt text with the value of the `name` property of the poster. 1. Each `img` element should have a `src` attribute set to the `avatar_template` of the matched user. If `avatar_template` starts with `/`, prepend `avatarUrl` directly to it. 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 objects from the `topics` array, which is 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 a `div` element with class `avatar-container` that contains the images returned by the `avatars` function called with `posters` and `users` as arguments. - 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. You should specifically use `console.log` for this. # --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`. ```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(actual, expected); ``` When the time difference between the time passed as argument and the current time is `60` minutes, `timeAgo` should return `1h ago`. ```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(actual, expected); ``` When the time difference between the time passed as argument and the current time is `115` minutes, `timeAgo` should return `1h ago`. ```js const generateTime = () => { const currentTime = new Date(); return new Date(currentTime - 1000 * (60 * 115)).toISOString(); }; const expected = '1h ago'; const actual = timeAgo(generateTime()); assert.equal(actual, expected); ``` When the time difference between the time passed as argument and the current time is `15` hours, `timeAgo` should return `15h ago`. ```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(actual, expected); ``` When the time difference between the time passed as argument and the current time is `24` hours, `timeAgo` should return `1d ago`. ```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(actual, expected); ``` When the time difference between the time passed as argument and the current time is `46` hours, `timeAgo` should return `1d ago`. ```js const generateTime = () => { const currentTime = new Date(); return new Date(currentTime - 1000 * 60 * 60 * 46).toISOString(); }; const expected = '1d ago'; const actual = timeAgo(generateTime()); assert.equal(actual, expected); ``` When the time difference between the time passed as argument and the current time is `3` days, `timeAgo` should return `3d ago`. ```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(actual, expected); ``` 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.trim(), /^<\s*a.+?>\s*Career Advice\s*<\/a>$/); // prevent hardcoding actual = forumCategory(409); assert.match(actual.trim(), /^<\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.trim(), /^<\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 general"`. ```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/QuincyLarson_{size}.png', id: 6, name: 'Quincy Larson', username: 'QuincyLarson' }, { avatar_template: '/user_avatar/jwilkins.oboe_{size}.png', id: 285941, name: 'Jessica Wilkins', username: 'jwilkins.oboe' }, { avatar_template: '/user_avatar/ilenia_{size}.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/QuincyLarson_{size}.png', id: 6, name: 'Quincy Larson', username: 'QuincyLarson' }, { avatar_template: '/user_avatar/jwilkins.oboe_{size}.png', id: 285941, name: 'Jessica Wilkins', username: 'jwilkins.oboe' }, { avatar_template: '/user_avatar/ilenia_{size}.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\1/); assert.match(matches[1], /alt=('|")Jessica Wilkins\1/); assert.match(matches[2], /alt=('|")Ilenia\1/); ``` 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/QuincyLarson_{size}.png', id: 6, name: 'Quincy Larson', username: 'QuincyLarson' }, { avatar_template: '/user_avatar/jwilkins.oboe_{size}.png', id: 285941, name: 'Jessica Wilkins', username: 'jwilkins.oboe' }, { avatar_template: '/user_avatar/ilenia_{size}.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 should have a `src` attribute set to the `avatar_template` of the matched user. If `avatar_template` starts with `/`, prepend `avatarUrl` directly to it. ```js const posters = [{ user_id: 6 }, { user_id: 285941 }, { user_id: 170865 }]; const users = [ { avatar_template: '/user_avatar/QuincyLarson_{size}.png', id: 6, name: 'Quincy Larson', username: 'QuincyLarson' }, { avatar_template: '/user_avatar/jwilkins.oboe_{size}.png', id: 285941, name: 'Jessica Wilkins', username: 'jwilkins.oboe' }, { avatar_template: '/user_avatar/ilenia_{size}.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:\/\/cdn\.freecodecamp\.org\/curriculum\/forum-latest\/user_avatar\/QuincyLarson_30\.png\1/ ); assert.match( matches[1], /src=('|")https:\/\/cdn\.freecodecamp\.org\/curriculum\/forum-latest\/user_avatar\/jwilkins\.oboe_30\.png\1/ ); assert.match( matches[2], /src=('|")https:\/\/cdn\.freecodecamp\.org\/curriculum\/forum-latest\/user_avatar\/ilenia_30\.png\1/ ); ``` 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, using `console.log`. ```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/QuincyLarson_{size}.png', id: 6, name: 'Quincy Larson', username: 'QuincyLarson' }, { avatar_template: '/user_avatar/jwilkins.oboe_{size}.png', id: 285941, name: 'Jessica Wilkins', username: 'jwilkins.oboe' }, { avatar_template: '/user_avatar/ilenia_{size}.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/QuincyLarson_{size}.png', id: 6, name: 'Quincy Larson', username: 'QuincyLarson' }, { avatar_template: '/user_avatar/jwilkins.oboe_{size}.png', id: 285941, name: 'Jessica Wilkins', username: 'jwilkins.oboe' }, { avatar_template: '/user_avatar/ilenia_{size}.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/QuincyLarson_{size}.png', id: 6, name: 'Quincy Larson', username: 'QuincyLarson' }, { avatar_template: '/user_avatar/jwilkins.oboe_{size}.png', id: 285941, name: 'Jessica Wilkins', username: 'jwilkins.oboe' }, { avatar_template: '/user_avatar/ilenia_{size}.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/QuincyLarson_{size}.png', id: 6, name: 'Quincy Larson', username: 'QuincyLarson' }, { avatar_template: '/user_avatar/jwilkins.oboe_{size}.png', id: 285941, name: 'Jessica Wilkins', username: 'jwilkins.oboe' }, { avatar_template: '/user_avatar/ilenia_{size}.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://cdn.freecodecamp.org/curriculum/forum-latest/user_avatar/QuincyLarson_30.png' ); assert.equal(imgs1[0].alt, 'Quincy Larson'); assert.equal( imgs1[1].src, 'https://cdn.freecodecamp.org/curriculum/forum-latest/user_avatar/ilenia_30.png' ); assert.equal(imgs1[1].alt, 'Ilenia'); assert.equal( imgs1[2].src, 'https://cdn.freecodecamp.org/curriculum/forum-latest/user_avatar/jwilkins.oboe_30.png' ); assert.equal(imgs1[2].alt, 'Jessica Wilkins'); const imgs2 = div2.querySelectorAll('img'); assert.lengthOf(imgs2, 1); assert.equal( imgs2[0].src, 'https://cdn.freecodecamp.org/curriculum/forum-latest/user_avatar/ilenia_30.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/QuincyLarson_{size}.png', id: 6, name: 'Quincy Larson', username: 'QuincyLarson' }, { avatar_template: '/user_avatar/jwilkins.oboe_{size}.png', id: 285941, name: 'Jessica Wilkins', username: 'jwilkins.oboe' }, { avatar_template: '/user_avatar/ilenia_{size}.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/QuincyLarson_{size}.png', id: 6, name: 'Quincy Larson', username: 'QuincyLarson' }, { avatar_template: '/user_avatar/jwilkins.oboe_{size}.png', id: 285941, name: 'Jessica Wilkins', username: 'jwilkins.oboe' }, { avatar_template: '/user_avatar/ilenia_{size}.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/QuincyLarson_{size}.png', id: 6, name: 'Quincy Larson', username: 'QuincyLarson' }, { avatar_template: '/user_avatar/jwilkins.oboe_{size}.png', id: 285941, name: 'Jessica Wilkins', username: 'jwilkins.oboe' }, { avatar_template: '/user_avatar/ilenia_{size}.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

Topics Avatars Replies Views Activity
``` ```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://cdn.freecodecamp.org/curriculum/forum-latest'; 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: 'Back-End Development', className: 'backend' } }; ``` # --solutions-- ```html fCC Forum Leaderboard

Latest Topics

Topics Avatars Replies Views Activity
``` ```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://cdn.freecodecamp.org/curriculum/forum-latest'; 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: 'Back-End 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(''); }; ```