Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | 32x 8x 24x 39x 6x 6x 6x 5x 4x 4x 39x 24x 2x 10x | /**
* EI-2966 — Achievements sorting + summary helper.
*
* Pure helper for the AchievementsCard widget. Given the
* `achievements` array returned by `GET /v1/student/dashboard/
* achievements/fetch`, returns:
* - `sorted`: earned badges first (sorted by earned_at ASC, with
* null earned_at last among earned), then locked badges in the
* server-returned order.
* - `earnedCount` / `lockedCount` for the card's summary chip.
*
* The original server array order is preserved within each group so
* the FE doesn't impose its own opinion on badge ordering beyond
* "earned first".
*/
export function sortAchievements(achievements) {
if (!Array.isArray(achievements) || achievements.length === 0) {
return { sorted: [], earnedCount: 0, lockedCount: 0 };
}
const earned = achievements
.filter((a) => a && a.earned)
.sort((a, b) => {
const aAt = a.earned_at || "";
const bAt = b.earned_at || "";
// Empty string sorts LAST among earned (so dateless-earned go after dated ones).
if (!aAt && !bAt) return 0;
if (!aAt) return 1;
Iif (!bAt) return -1;
return aAt.localeCompare(bAt);
});
const locked = achievements.filter((a) => a && !a.earned);
return {
sorted: [...earned, ...locked],
earnedCount: earned.length,
lockedCount: locked.length,
};
}
/**
* Map a server-side `icon` slug to a stable identifier used by the
* FE for selecting the MUI icon component. Kept as a separate
* helper so the slug → component lookup table is unit-testable.
*/
export const ACHIEVEMENT_ICON_SLUGS = new Set([
"rocket",
"star",
"trophy",
"flame",
"medal",
]);
export function isKnownIconSlug(slug) {
return typeof slug === "string" && ACHIEVEMENT_ICON_SLUGS.has(slug);
}
|