Back to Freecodecamp

Build an fCC Forum Leaderboard

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

latest39.6 KB
Original Source

--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.

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.
  2. 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.
  3. You should have a function named viewCount that takes the number of views of a post as the argument.
  4. 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.
  5. You should have a function named forumCategory that takes the id of a selected category as the argument.
  6. The forumCategory function should verify that the selected category id is a property of the allCategories object and should return a string containing an anchor element with:
    • the text of the category key of the selected category.
    • a class of category followed by the className property of the selected category.
    • an href with the value of <forumCategoryUrl>/<className>/<id>, where <className> is the className property of the selected category and id is the argument passed to forumCategory.
  7. 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.
  8. You should have a function named avatars that takes two arrays representing posters and users, respectively.
  9. 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.
  10. The avatars function should set each avatar's size by accessing the avatar_template property and replacing {size} with 30.
  11. Each image element should have an alt text with the value of the name property of the poster.
  12. Each image element should have a source with the value of the avatar_template property of the poster. In case avatar_template contains a relative path, you should set the source to <avatarUrl>/<avatar_template>.
  13. You should have a function named showLatestPosts that takes a single parameter.
  14. 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
  15. The showLatestPosts should set the inner HTML of #posts-container to a string made by joining tr elements, one for each item in topics.
  16. 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.
  17. You should have an async function named fetchData.
  18. The fetchData function should request data from forumLatest and call showLatestPosts passing it the response parsed as JSON.
  19. 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, /^<\s*a.+?>\s*Career Advice\s*<\/a>$/);
// prevent hardcoding

actual = forumCategory(409);
assert.match(actual, /^<\s*a.+?>\s*Project Feedback\s*<\/a>$/);

forumCategory(299) should return a string containing an anchor element with href="https://forum.freecodecamp.org/c/career/299".

js
let actual = forumCategory(299);
assert.match(
  actual,
  /href=("|')https:\/\/forum\.freecodecamp\.org\/c\/career\/299\1/
);

// prevent hardcoding
actual = forumCategory(409);
assert.match(
  actual,
  /href=("|')https:\/\/forum\.freecodecamp\.org\/c\/feedback\/409\1/
);

forumCategory(299) should return a string containing an anchor element with class="category career".

js
let actual = forumCategory(299);
assert.match(actual, /class=("|')category\s+career\1/);

// prevent hardcoding
actual = forumCategory(409);
assert.match(actual, /class=("|')category\s+feedback\1/);

forumCategory(200) should return a string containing an anchor element with the text General.

js
const actual = forumCategory(200);
assert.match(actual, /^<\s*a.+?>\s*General\s*<\/a>$/);

forumCategory(200) should return a string containing an anchor element with href="https://forum.freecodecamp.org/c/general/200".

js
let actual = forumCategory(200);
assert.match(
  actual,
  /href=("|')https:\/\/forum\.freecodecamp\.org\/c\/general\/200/
);

actual = forumCategory(220);
assert.match(
  actual,
  /href=("|')https:\/\/forum\.freecodecamp\.org\/c\/general\/220/
);

forumCategory(200) should return a string containing an anchor element with class="category 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 in the string returned by the avatars function should have the src with the value of the avatar_template property of the poster. In case avatar_template contains a relative path, it should be set to <avatarUrl>/<avatar_template>.

js
const posters = [{ user_id: 6 }, { user_id: 285941 }, { user_id: 170865 }];
const users = [
  {
    avatar_template:
      '/user_avatar/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>
        
      </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>
        
      </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 ``;
      }
    })
    .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('');
};