diff --git a/frontend/src/ts/elements/profile.ts b/frontend/src/ts/elements/profile.ts index bae864a9e..a280120b8 100644 --- a/frontend/src/ts/elements/profile.ts +++ b/frontend/src/ts/elements/profile.ts @@ -1,5 +1,5 @@ import * as DB from "../db"; -import { format } from "date-fns/format"; +import { format as dateFormat } from "date-fns/format"; import { differenceInDays } from "date-fns/differenceInDays"; import * as Misc from "../utils/misc"; import * as Numbers from "@monkeytype/util/numbers"; @@ -11,12 +11,14 @@ import * as ActivePage from "../states/active-page"; import { formatDistanceToNowStrict } from "date-fns/formatDistanceToNowStrict"; import { getHtmlByUserFlags } from "../controllers/user-flag-controller"; import Format from "../utils/format"; -import { UserProfile, RankAndCount } from "@monkeytype/schemas/users"; -import { abbreviateNumber, convertRemToPixels } from "../utils/numbers"; +import { UserProfile } from "@monkeytype/schemas/users"; +import { convertRemToPixels } from "../utils/numbers"; import { secondsToString } from "../utils/date-and-time"; import { getAuthenticatedUser } from "../firebase"; import { Snapshot } from "../constants/default-snapshot"; import { getAvatarElement } from "../utils/discord-avatar"; +import { formatXp } from "../utils/levels"; +import { formatTopPercentage } from "../utils/misc"; type ProfileViewPaths = "profile" | "account"; type UserProfileOrSnapshot = UserProfile | Snapshot; @@ -87,7 +89,8 @@ export async function update( updateNameFontSize(where); }, 10); - const joinedText = "Joined " + format(profile.addedAt ?? 0, "dd MMM yyyy"); + const joinedText = + "Joined " + dateFormat(profile.addedAt ?? 0, "dd MMM yyyy"); const creationDate = new Date(profile.addedAt); const diffDays = differenceInDays(new Date(), creationDate); const balloonText = `${diffDays} day${diffDays !== 1 ? "s" : ""} ago`; @@ -184,21 +187,9 @@ export async function update( .attr("aria-label", hoverText) .attr("data-balloon-break", ""); - let completedPercentage = ""; - let restartRatio = ""; - if ( - profile.typingStats.completedTests !== undefined && - profile.typingStats.startedTests !== undefined - ) { - completedPercentage = Math.floor( - (profile.typingStats.completedTests / profile.typingStats.startedTests) * - 100 - ).toString(); - restartRatio = ( - (profile.typingStats.startedTests - profile.typingStats.completedTests) / - profile.typingStats.completedTests - ).toFixed(1); - } + const { completedPercentage, restartRatio } = Misc.formatTypingStatsRatio( + profile.typingStats + ); const typingStatsEl = details.find(".typingStats"); typingStatsEl @@ -449,17 +440,3 @@ const throttledEvent = throttle(1000, () => { $(window).on("resize", () => { throttledEvent(); }); - -function formatTopPercentage(lbRank: RankAndCount): string { - if (lbRank.rank === undefined) return "-"; - if (lbRank.rank === 1) return "GOAT"; - return "Top " + Numbers.roundTo2((lbRank.rank / lbRank.count) * 100) + "%"; -} - -function formatXp(xp: number): string { - if (xp < 1000) { - return Math.round(xp).toString(); - } else { - return abbreviateNumber(xp); - } -} diff --git a/frontend/src/ts/pages/friends.ts b/frontend/src/ts/pages/friends.ts index 577213bab..acad6fe8e 100644 --- a/frontend/src/ts/pages/friends.ts +++ b/frontend/src/ts/pages/friends.ts @@ -2,18 +2,25 @@ import Page from "./page"; import * as Skeleton from "../utils/skeleton"; import { SimpleModal } from "../utils/simple-modal"; import Ape from "../ape"; -import { formatDuration } from "date-fns/formatDuration"; -import { intervalToDuration } from "date-fns"; +import { + FormatDurationOptions, + intervalToDuration, + format as dateFormat, + formatDuration, +} from "date-fns"; import * as Notifications from "../elements/notifications"; import { isSafeNumber } from "@monkeytype/util/numbers"; import { getHTMLById as getBadgeHTMLbyId } from "../controllers/badge-controller"; -import { getXpDetails } from "../utils/levels"; +import { formatXp, getXpDetails } from "../utils/levels"; import { secondsToString } from "../utils/date-and-time"; import { PersonalBest } from "@monkeytype/schemas/shared"; import Format from "../utils/format"; import { getHtmlByUserFlags } from "../controllers/user-flag-controller"; import { Friend } from "@monkeytype/schemas/friends"; import { SortedTable } from "../utils/sorted-table"; +import { getAvatarElement } from "../utils/discord-avatar"; +import { formatTypingStatsRatio } from "../utils/misc"; +import { getLanguageDisplayString } from "../utils/strings"; const pageElement = $(".page.pageFriends"); @@ -120,11 +127,8 @@ async function fetchFriends(): Promise { } function buildFriendRow(entry: Friend): HTMLTableRowElement { - let avatar = `
`; - if (entry.discordAvatar !== undefined) { - avatar = `
`; - } const xpDetails = getXpDetails(entry.xp ?? 0); + const testStats = formatTypingStatsRatio(entry); const top15 = formatPb(entry.top15); const top60 = formatPb(entry.top60); @@ -134,11 +138,12 @@ function buildFriendRow(entry: Friend): HTMLTableRowElement { element.innerHTML = `
-
${avatar}
+
${entry.name} -
+ }?isUid" class="entryName" uid=${entry.uid} router-link>${ + entry.name + }
${getHtmlByUserFlags(entry)} ${ isSafeNumber(entry.badgeId) @@ -148,17 +153,38 @@ function buildFriendRow(entry: Friend): HTMLTableRowElement {
- ${formatAge(entry.addedAt)} - ${xpDetails.level} - ${entry.completedTests}/${entry.startedTests} + ${formatAge(entry.addedAt, ["years", "days"])} + + ${xpDetails.level} + + ${ + entry.completedTests + }/${entry.startedTests} ${secondsToString( Math.round(entry.timeTyping ?? 0), true, true )} - ${entry.streak !== undefined ? entry.streak.length + " days" : ""} - ${top15?.wpm}
${top15?.acc}
- ${top60?.wpm}
${top60?.acc}
+ + ${formatStreak(entry.streak?.length)} + + ${ + top15?.wpm + }
${top15?.acc}
+ ${ + top60?.wpm + }
${top60?.acc}
`; + + element + .querySelector(".avatarPlaceholder") + ?.replaceWith(getAvatarElement(entry)); return element; } -function formatAge(timestamp?: number): string { +function formatAge( + timestamp: number | undefined, + format: FormatDurationOptions["format"] = ["days", "hours", "minutes"] +): string { if (timestamp === undefined) return ""; const formatted = formatDuration( intervalToDuration({ start: timestamp, end: Date.now() }), - { format: ["days", "hours", "minutes"] } + { format } ); - return (formatted !== "" ? formatted : "less then a minute") + " ago"; + return formatted !== "" ? formatted : "less then a minute"; } function formatPb(entry?: PersonalBest): @@ -186,17 +219,38 @@ function formatPb(entry?: PersonalBest): acc: string; raw: string; con: string; + details: string; } | undefined { if (entry === undefined) { return undefined; } - return { + const result = { wpm: Format.typingSpeed(entry.wpm, { showDecimalPlaces: true }), acc: Format.percentage(entry.acc, { showDecimalPlaces: true }), raw: Format.typingSpeed(entry.raw, { showDecimalPlaces: true }), con: Format.percentage(entry.consistency, { showDecimalPlaces: true }), + details: "", }; + + result.details = [ + `${getLanguageDisplayString(entry.language)}`, + `${result.wpm} wpm`, + `${result.raw} raw`, + `${result.acc} acc`, + `${result.con} con`, + `${dateFormat(entry.timestamp, "dd MMM yyyy")}`, + ].join("\n"); + + return result; +} + +function formatStreak(length?: number, prefix?: string): string { + return length !== undefined + ? `${prefix !== undefined ? prefix + " " : ""}${length} ${ + length === 1 ? "day" : "days" + } ` + : ""; } $("#friendAdd").on("click", () => { diff --git a/frontend/src/ts/utils/levels.ts b/frontend/src/ts/utils/levels.ts index 6138b3a12..1ce16972f 100644 --- a/frontend/src/ts/utils/levels.ts +++ b/frontend/src/ts/utils/levels.ts @@ -1,3 +1,5 @@ +import { abbreviateNumber } from "./numbers"; + /** * Calculates the level based on the total XP. * This is the inverse of the function getTotalXpToReachLevel() @@ -51,3 +53,11 @@ export function getXpDetails(totalXp: number): XPDetails { levelMaxXp: getLevelMaxXp(level), }; } + +export function formatXp(xp: number): string { + if (xp < 1000) { + return Math.round(xp).toString(); + } else { + return abbreviateNumber(xp); + } +} diff --git a/frontend/src/ts/utils/misc.ts b/frontend/src/ts/utils/misc.ts index 5920e7694..5ba35eed1 100644 --- a/frontend/src/ts/utils/misc.ts +++ b/frontend/src/ts/utils/misc.ts @@ -4,6 +4,8 @@ import { lastElementFromArray } from "./arrays"; import { Config } from "@monkeytype/schemas/configs"; import { Mode, Mode2, PersonalBests } from "@monkeytype/schemas/shared"; import { Result } from "@monkeytype/schemas/results"; +import { RankAndCount } from "@monkeytype/schemas/users"; +import { roundTo2 } from "@monkeytype/util/numbers"; export function whorf(speed: number, wordlen: number): number { return Math.min( @@ -761,4 +763,31 @@ export function scrollToCenterOrTop(el: HTMLElement | null): void { }); } +export function formatTopPercentage(lbRank: RankAndCount): string { + if (lbRank.rank === undefined) return "-"; + if (lbRank.rank === 1) return "GOAT"; + return "Top " + roundTo2((lbRank.rank / lbRank.count) * 100) + "%"; +} + +export function formatTypingStatsRatio(stats: { + startedTests?: number; + completedTests?: number; +}): { + completedPercentage: string; + restartRatio: string; +} { + if (stats.completedTests === undefined || stats.startedTests === undefined) { + return { completedPercentage: "", restartRatio: "" }; + } + return { + completedPercentage: Math.floor( + (stats.completedTests / stats.startedTests) * 100 + ).toString(), + restartRatio: ( + (stats.startedTests - stats.completedTests) / + stats.completedTests + ).toFixed(1), + }; +} + // DO NOT ALTER GLOBAL OBJECTSONSTRUCTOR, IT WILL BREAK RESULT HASHES