diff --git a/frontend/src/html/pages/friends.html b/frontend/src/html/pages/friends.html index 7c842bfc7..5f34914a6 100644 --- a/frontend/src/html/pages/friends.html +++ b/frontend/src/html/pages/friends.html @@ -66,34 +66,27 @@

Something went wrong

+ - - - + + + - - - + + - diff --git a/frontend/src/ts/pages/friends.ts b/frontend/src/ts/pages/friends.ts index 3f64e08a3..c617aa507 100644 --- a/frontend/src/ts/pages/friends.ts +++ b/frontend/src/ts/pages/friends.ts @@ -13,25 +13,10 @@ import { PersonalBest } from "@monkeytype/contracts/schemas/shared"; import Format from "../utils/format"; import { getHtmlByUserFlags } from "../controllers/user-flag-controller"; import { Friend } from "@monkeytype/contracts/schemas/friends"; +import { SortedTable } from "../utils/sorted-table"; const pageElement = $(".page.pageFriends"); -type Sort = { - property: string; - descending: boolean; -}; - -type State = { - friends: { - data: Array; - sort: Sort; - }; -}; - -const state: State = { - friends: { data: [], sort: { property: "name", descending: false } }, -}; - const addFriendModal = new SimpleModal({ id: "addFriend", title: "Add a friend", @@ -101,44 +86,51 @@ async function updatePendingRequests(): Promise { async function fetchFriends(): Promise { $(".pageFriends .friends .loading").removeClass("hidden"); const result = await Ape.friends.getFriends(); + $(".pageFriends .friends .loading").addClass("hidden"); + if (result.status !== 200) { $(".pageFriends .friends .error").removeClass("hidden"); $(".pageFriends .friends .error p").html(result.body.message); - } else { - $(".pageFriends .friends .error").addClass("hidden"); - state.friends.data = result.body.data; + return; } - $(".pageFriends .friends .loading").addClass("hidden"); -} -async function updateFriends(): Promise { - if (state.friends.data.length === 0) { + $(".pageFriends .friends .error").addClass("hidden"); + + if (result.body.data.length === 0) { $(".pageFriends .friends table").addClass("hidden"); $(".pageFriends .friends .nodata").removeClass("hidden"); } else { $(".pageFriends .friends table").removeClass("hidden"); $(".pageFriends .friends .nodata").addClass("hidden"); - const html = state.friends.data - .map((entry) => { - let avatar = `
`; - if (entry.discordAvatar !== undefined) { - avatar = `
`; - } - const xpDetails = getXpDetails(entry.xp ?? 0); + new SortedTable({ + table: ".pageFriends .friends table", + data: result.body.data, + buildRow: buildFriendRow, + initialSort: { property: "name", descending: false }, + }); + } +} - const top15 = formatPb(entry.top15); - const top60 = formatPb(entry.top60); +function buildFriendRow(entry: Friend): HTMLTableRowElement { + let avatar = `
`; + if (entry.discordAvatar !== undefined) { + avatar = `
`; + } + const xpDetails = getXpDetails(entry.xp ?? 0); - return `
+ const top15 = formatPb(entry.top15); + const top60 = formatPb(entry.top60); + + const element = document.createElement("tr"); + element.dataset["id"] = entry.friendRequestId; + element.innerHTML = ``; - }) - .join("\n"); - - $(".pageFriends .friends tbody").html(html); - } + return element; } function formatAge(timestamp?: number): string { @@ -204,50 +192,6 @@ function formatPb(entry?: PersonalBest): }; } -function getValueByPath(obj: unknown, path: string): unknown { - return path.split(".").reduce((acc, key) => { - // oxlint-disable-next-line no-explicit-any - // @ts-expect-error this is fine - return acc !== null && acc !== undefined ? acc[key] : undefined; - }, obj); -} - -function sortFriends({ property, descending }: Sort): void { - // Removes styling from previous sorting requests: - $(".friends td").removeClass("headerSorted"); - $(".friends td").children("i").remove(); - $(`.friends td[data-property="${property}"]`) - .addClass("headerSorted") - .append( - `` - ); - - state.friends.data.sort((a, b) => { - const valA = getValueByPath(a, property); - const valB = getValueByPath(b, property); - - let result = 0; - - if (valA === undefined && valB !== undefined) { - return descending ? 1 : -1; - } else if (valA !== undefined && valB === undefined) { - return descending ? -1 : 1; - } - - if (typeof valA === "string" && typeof valB === "string") { - result = valA.localeCompare(valB); - } - - if (typeof valA === "number" && typeof valB === "number") { - result = valA - valB; - } - - return descending ? -result : result; - }); -} - $("#friendAdd").on("click", () => { addFriendModal.show(undefined, {}); }); @@ -299,22 +243,6 @@ $(".pageFriends .pendingRequests table").on("click", async (e) => { } }); -$(".pageFriends .friends thead td.sortable").on("click", async (e) => { - const property = e.currentTarget.dataset["property"]; - if (property === undefined) return; - - if (property === state.friends.sort.property) { - state.friends.sort.descending = !state.friends.sort.descending; - } else { - state.friends.sort = { - property, - descending: false, - }; - } - sortFriends(state.friends.sort); - await updateFriends(); -}); - export const page = new Page({ id: "friends", display: "Friends", @@ -328,8 +256,6 @@ export const page = new Page({ await updatePendingRequests(); await fetchFriends(); - sortFriends({ property: "name", descending: false }); - await updateFriends(); }, }); diff --git a/frontend/src/ts/utils/sorted-table.ts b/frontend/src/ts/utils/sorted-table.ts new file mode 100644 index 000000000..bf6c62286 --- /dev/null +++ b/frontend/src/ts/utils/sorted-table.ts @@ -0,0 +1,106 @@ +type Sort = { property: string; descending: boolean }; + +export class SortedTable { + private data: { source: T; element: HTMLTableRowElement }[]; + private table: JQuery; + private sort?: Sort; + + constructor({ + table, + data, + buildRow, + initialSort, + }: { + table: string; + data: T[]; + buildRow: (entry: T) => HTMLTableRowElement; + initialSort?: Sort; + }) { + this.table = $(table); + if (this.table === undefined) + throw new Error(`No element found for ${table}`); + + //render content + this.data = data.map((source) => ({ source, element: buildRow(source) })); + if (initialSort !== undefined) { + this.sort = initialSort; + this.doSort(); + } + + //init rows + for (const col of this.table.find(`td[data-sort-property]`)) { + col.classList.add("sortable"); + col.setAttribute("type", "button"); + col.onclick = (e: MouseEvent) => { + const target = e.currentTarget as HTMLElement; + const property = target.dataset["sortProperty"] as string; + if (property === undefined) return; + + if (this.sort === undefined || property !== this.sort.property) { + this.sort = { property, descending: false }; + } else { + this.sort.descending = !this.sort?.descending; + } + + this.doSort(); + this.updateBody(); + }; + } + + //fill table body + this.updateBody(); + } + + private doSort(): void { + if (this.sort === undefined) return; + + const { property, descending } = this.sort; + // Removes styling from previous sorting requests: + this.table.find("thead td").removeClass("headerSorted"); + this.table.find("thead td").children("i").remove(); + this.table + .find(`thead td[data-sort-property="${property}"]`) + .addClass("headerSorted") + .append( + `` + ); + + this.data.sort((a, b) => { + const valA = getValueByPath(a.source, property); + const valB = getValueByPath(b.source, property); + + let result = 0; + + if (valA === undefined && valB !== undefined) { + return descending ? 1 : -1; + } else if (valA !== undefined && valB === undefined) { + return descending ? -1 : 1; + } + + if (typeof valA === "string" && typeof valB === "string") { + result = valA.localeCompare(valB); + } + + if (typeof valA === "number" && typeof valB === "number") { + result = valA - valB; + } + + return descending ? -result : result; + }); + } + private updateBody(): void { + const body = this.table.find("tbody"); + body.empty(); + body.append(this.data.map((data) => data.element)); + } +} + +function getValueByPath(obj: unknown, path: string): unknown { + return path.split(".").reduce((acc, key) => { + // oxlint-disable-next-line no-explicit-any + // @ts-expect-error this is fine + return acc !== null && acc !== undefined ? acc[key] : undefined; + }, obj); +}
name - friend since - levelnamefriend sincelevel tests - time typing - - streak - + time typingstreak 15s wpm
accuracy
+ 60s wpm
accuracy
${avatar}
${ - entry.name - } + entry.uid + }?isUid" class="entryName" uid=${entry.uid} router-link>${entry.name}
${getHtmlByUserFlags(entry)} ${ @@ -169,11 +161,7 @@ async function updateFriends(): Promise {