mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
271a69bef5
Co-authored-by: Venkat <venkat@Venkats-MacBook-Pro.local>
1595 lines
41 KiB
Markdown
1595 lines
41 KiB
Markdown
---
|
||
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('');
|
||
};
|
||
```
|