Co-authored-by: Venkat <venkat@Venkats-MacBook-Pro.local>
41 KiB
id, title, challengeType, dashedName, demoType
| id | title | challengeType | dashedName | demoType |
|---|---|---|---|---|
| 673c91f0b934834bc4a3ecc2 | Build an fCC Forum Leaderboard | 25 | build-an-fcc-forum-leaderboard | onClick |
--description--
In this lab, you will build a freeCodeCamp forum leaderboard that displays the latest topics, users, and replies from the freeCodeCamp forum. 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:
{
"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:
- You should have a function named
timeAgothat takes a timestamp in the ISO 8601 format as the argument. - The
timeAgofunction should compute the time difference between the time passed as an argument and the current time and return:xm ago(xrepresents minutes) if the amount of minutes that have passed is less than60.xh ago(xrepresents hours) if the amount of hours that have passed is less than24.xd ago(xrepresents days) otherwise.
- You should have a function named
viewCountthat takes the number of views of a post as the argument. - If the value of the views passed as the argument is greater than or equal to
1000, theviewCountfunction should return a string with the views value divided by1000, rounded down to the nearest whole number and the letterkappended to it. Otherwise, it should return the views value. - You should have a function named
forumCategorythat takes the id of a selected category as the argument. - The
forumCategoryfunction should verify that the selected category id is a property of theallCategoriesobject and should return a string containing an anchor element with:- the text of the
categorykey of the selected category. - a class of
categoryfollowed by theclassNameproperty of the selected category. - an
hrefformed by appending<className>/<id>toforumCategoryUrl(note:forumCategoryUrlalready ends with/, so do not add an extra/separator), where<className>is theclassNameproperty of the selected category andidis the argument passed toforumCategory.
- the text of the
- If the
allCategoriesobject does not have the selected category id as its property,categoryshould be indicated asGeneralandclassNameshould be indicated asgeneral. - You should have a function named
avatarsthat takes two arrays representing posters and users, respectively. - The
avatarsfunction should return a string made by joiningimgelements, one for eachuser_idin thepostersarray. Find theimgURL by looking up theuser_idproperty in thepostersarray and find the matchingidproperty in theusersarray. - The
avatarsfunction should set each avatar's size by accessing theavatar_templateproperty of the matched user object (found in theusersarray by comparing itsidto theuser_idinposters) and replacing the{size}placeholder in the URL string with30. - Each image element should have an alt text with the value of the
nameproperty of the poster. - Each
imgelement should have asrcattribute set to theavatar_templateof the matched user. Ifavatar_templatestarts with/, prependavatarUrldirectly to it. - You should have a function named
showLatestPoststhat takes a single parameter. - The
showLatestPostsshould extract theusersandtopic_listproperties from the object passed as argument. Also, it should process the following properties of the objects from thetopicsarray, which is contained intopic_list:id: the id of the posttitle: the title of the postviews: the number of views of the postposts_count: the number of replies to the topicslug: the slug of the postposters: the posters for that topiccategory_id: an integer indicating the category id for the postbumped_at: a timestamp in the ISO 8601 format
- The
showLatestPostsshould set the inner HTML of#posts-containerto a string made by joiningtrelements, one for each item intopics. - Each
trelement should have fivetdelements in it:- a
tdcontaining two anchor elements, one with the class ofpost-title, anhrefof<forumTopicUrl><slug>/<id>, an anchor text of<title>, and one obtained by callingforumCategorywithcategory_id. - a
tdcontaining adivelement with classavatar-containerthat contains the images returned by theavatarsfunction called withpostersandusersas arguments. - a
tdcontaining the number of replies to the post. Hint: useposts_count - 1. - a
tdcontaining the number of views of the post. - a
tdcontaining the time passed since the last activity.
- a
- You should have an async function named
fetchData. - The
fetchDatafunction should request data fromforumLatestand callshowLatestPostspassing it the response parsed as JSON. - If there's an error when fetching data, the
fetchDatafunction should log the error to the console. You should specifically useconsole.logfor this.
--hints--
You should have a function named timeAgo that takes a single argument.
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.
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.
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.
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.
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.
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.
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.
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.
assert.isFunction(viewCount);
assert.lengthOf(viewCount, 1);
viewCount(597) should return 597.
assert.strictEqual(597, viewCount(597));
viewCount(1000) should return 1k.
assert.equal('1k', viewCount(1000));
viewCount(2730) should return 2k.
assert.equal('2k', viewCount(2730));
You should have a function named forumCategory that takes a single argument.
assert.isFunction(forumCategory);
assert.lengthOf(forumCategory, 1);
forumCategory(299) should return a string containing an anchor element with the text Career Advice.
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".
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".
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.
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".
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".
const actual = forumCategory(200);
assert.match(actual, /class=("|')category\s+general\1/);
You should have a function named avatars that takes two arguments.
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.
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.
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.
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.
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.
assert.isFunction(showLatestPosts);
assert.lengthOf(showLatestPosts, 1);
You should have a function named fetchData.
assert.isFunction(fetchData);
Your fetchData function should request data from https://cdn.freecodecamp.org/curriculum/forum-latest/latest.json.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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--
<!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>
* {
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;
}
}
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--
<!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>
* {
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;
}
}
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('');
};