Files
freeCodeCamp/curriculum/challenges/english/blocks/lab-fcc-forum-leaderboard/673c91f0b934834bc4a3ecc2.md
T

1595 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 `<className>/<id>` to `forumCategoryUrl` (note: `forumCategoryUrl` already ends with `/`, so do not add an extra `/` separator), where `<className>` 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 `<forumTopicUrl><slug>/<id>`, an anchor text of `<title>`, 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</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<header>
<nav>
<img
class="fcc-logo"
src="https://cdn.freecodecamp.org/platform/universal/fcc_primary.svg"
alt="freeCodeCamp logo"
/>
</nav>
<h1 class="title">Latest Topics</h1>
</header>
<main>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th id="topics">Topics</th>
<th id="avatars">Avatars</th>
<th id="replies">Replies</th>
<th id="views">Views</th>
<th id="activity">Activity</th>
</tr>
</thead>
<tbody id="posts-container"></tbody>
</table>
</div>
</main>
<script src="./script.js"></script>
</body>
</html>
```
```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
<!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</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<header>
<nav>
<img
class="fcc-logo"
src="https://cdn.freecodecamp.org/platform/universal/fcc_primary.svg"
alt="freeCodeCamp logo"
/>
</nav>
<h1 class="title">Latest Topics</h1>
</header>
<main>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th id="topics">Topics</th>
<th id="avatars">Avatars</th>
<th id="replies">Replies</th>
<th id="views">Views</th>
<th id="activity">Activity</th>
</tr>
</thead>
<tbody id="posts-container"></tbody>
</table>
</div>
</main>
<script src="./script.js"></script>
</body>
</html>
```
```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 `<a href="${url}" class="${linkClass}" target="_blank">
${linkText}
</a>`;
};
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 `<img src="${userAvatarUrl}" alt="${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 `
<tr>
<td>
<a class="post-title" target="_blank" href="${forumTopicUrl}${slug}/${id}">
${title}
</a>
${forumCategory(category_id)}
</td>
<td>
<div class="avatar-container">
${avatars(posters, users)}
</div>
</td>
<td>${posts_count - 1}</td>
<td>${viewCount(views)}</td>
<td>${timeAgo(bumped_at)}</td>
</tr>`;
})
.join('');
};
```