From 837051f386ed9d9cddf206490eeb1323daf766bf Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Mon, 7 Jul 2025 12:47:20 +0200 Subject: [PATCH] feat: add friend requests frontend (@fehmer) --- frontend/src/html/header.html | 10 ++ frontend/src/html/pages/friends.html | 92 +++++++++++ frontend/src/index.html | 1 + frontend/src/styles/friends.scss | 2 + frontend/src/styles/index.scss | 2 +- .../src/ts/controllers/page-controller.ts | 2 + .../src/ts/controllers/route-controller.ts | 19 +++ frontend/src/ts/pages/account.ts | 2 +- frontend/src/ts/pages/friends.ts | 150 ++++++++++++++++++ frontend/src/ts/pages/page.ts | 3 +- frontend/src/ts/ready.ts | 3 + 11 files changed, 283 insertions(+), 3 deletions(-) create mode 100644 frontend/src/html/pages/friends.html create mode 100644 frontend/src/styles/friends.scss create mode 100644 frontend/src/ts/pages/friends.ts diff --git a/frontend/src/html/header.html b/frontend/src/html/header.html index 4cbd88403..6baaa3bfa 100644 --- a/frontend/src/html/header.html +++ b/frontend/src/html/header.html @@ -175,6 +175,16 @@ Account settings + + + + Friends + + + +
+
+
+ Pending Friend Requests + +
+
+ + + + + + + + + + + + + + + + + + + + +
userdateactions
Username3 days ago + + + + + +
+
+ + diff --git a/frontend/src/index.html b/frontend/src/index.html index 59cef67e0..f48a35a4f 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -50,6 +50,7 @@ + diff --git a/frontend/src/styles/friends.scss b/frontend/src/styles/friends.scss new file mode 100644 index 000000000..e09191e37 --- /dev/null +++ b/frontend/src/styles/friends.scss @@ -0,0 +1,2 @@ +.pageFriends { +} diff --git a/frontend/src/styles/index.scss b/frontend/src/styles/index.scss index 31c74d171..e54d06f47 100644 --- a/frontend/src/styles/index.scss +++ b/frontend/src/styles/index.scss @@ -1,5 +1,5 @@ @import "buttons", "fonts", "404", "ads", "about", "account", "animations", "banners", "caret", "commandline", "core", "footer", "inputs", "keymap", "login", "monkey", "nav", "notifications", "popups", "profile", "scroll", - "settings", "account-settings", "leaderboards", "test", "loading", + "settings", "account-settings", "leaderboards", "test", "loading", "friends", "media-queries"; diff --git a/frontend/src/ts/controllers/page-controller.ts b/frontend/src/ts/controllers/page-controller.ts index 90ff97724..1ddf4029d 100644 --- a/frontend/src/ts/controllers/page-controller.ts +++ b/frontend/src/ts/controllers/page-controller.ts @@ -9,6 +9,7 @@ import * as PageLogin from "../pages/login"; import * as PageLoading from "../pages/loading"; import * as PageProfile from "../pages/profile"; import * as PageProfileSearch from "../pages/profile-search"; +import * as Friends from "../pages/friends"; import * as Page404 from "../pages/404"; import * as PageLeaderboards from "../pages/leaderboards"; import * as PageAccountSettings from "../pages/account-settings"; @@ -186,6 +187,7 @@ export async function change( login: PageLogin.page, profile: PageProfile.page, profileSearch: PageProfileSearch.page, + friends: Friends.page, 404: Page404.page, accountSettings: PageAccountSettings.page, leaderboards: PageLeaderboards.page, diff --git a/frontend/src/ts/controllers/route-controller.ts b/frontend/src/ts/controllers/route-controller.ts index 739c4b92b..546fb7748 100644 --- a/frontend/src/ts/controllers/route-controller.ts +++ b/frontend/src/ts/controllers/route-controller.ts @@ -6,6 +6,7 @@ import { isFunboxActive } from "../test/funbox/list"; import * as TestState from "../test/test-state"; import * as Notifications from "../elements/notifications"; import { LoadingOptions } from "../pages/page"; +import { get as getServerOptions } from "../ape/server-configuration"; //source: https://www.youtube.com/watch?v=OstALBk-jTc // https://www.youtube.com/watch?v=OstALBk-jTc @@ -144,6 +145,24 @@ const routes: Route[] = [ }); }, }, + { + path: "/friends", + load: async (_params, options) => { + if (!isAuthAvailable()) { + await navigate("/", options); + return; + } + if (!isAuthenticated()) { + await navigate("/login", options); + return; + } + if (!getServerOptions()?.friends.enabled) { + await navigate("/", options); + return; + } + await PageController.change("friends", options); + }, + }, ]; export async function navigate( diff --git a/frontend/src/ts/pages/account.ts b/frontend/src/ts/pages/account.ts index 338c4e11c..87d88b6b8 100644 --- a/frontend/src/ts/pages/account.ts +++ b/frontend/src/ts/pages/account.ts @@ -1318,7 +1318,7 @@ ConfigEvent.subscribe((eventKey) => { } }); -export const page = new Page({ +export const page = new Page({ id: "account", element: $(".page.pageAccount"), path: "/account", diff --git a/frontend/src/ts/pages/friends.ts b/frontend/src/ts/pages/friends.ts new file mode 100644 index 000000000..789c344ef --- /dev/null +++ b/frontend/src/ts/pages/friends.ts @@ -0,0 +1,150 @@ +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 * as Notifications from "../elements/notifications"; + +const pageElement = $(".page.pageFriends"); + +const addFriendModal = new SimpleModal({ + id: "addFriend", + title: "Add a friend", + inputs: [{ placeholder: "user name", type: "text", initVal: "" }], + buttonText: "request", + onlineOnly: true, + execFn: async (_thisPopup, friendName) => { + const result = await Ape.friends.createRequest({ body: { friendName } }); + + if (result.status !== 200) { + return { + status: -1, + message: `Friend request failed: ${result.body.message}`, + }; + } else { + return { status: 1, message: `Request send to ${friendName}` }; + } + }, +}); + +async function updatePendingRequests(): Promise { + $(".pageFriends .pendingRequests .loading").removeClass("hidden"); + + const format = (timestamp: number): string => { + const formatted = formatDuration( + intervalToDuration({ start: timestamp, end: Date.now() }), + { format: ["days", "hours", "minutes"] } + ); + return formatted !== "" ? formatted : "less then a minute"; + }; + const result = await Ape.friends.getRequests({ + query: { status: "pending", type: "incoming" }, + }); + + if (result.status !== 200) { + $(".pageFriends .pendingRequests .error").removeClass("hidden"); + $(".pageFriends .pendingRequests .error p").html(result.body.message); + } else { + $(".pageFriends .pendingRequests .error").addClass("hidden"); + if (result.body.data.length === 0) { + $(".pageFriends .pendingRequests table").addClass("hidden"); + $(".pageFriends .pendingRequests .nodata").removeClass("hidden"); + } else { + $(".pageFriends .pendingRequests table").removeClass("hidden"); + $(".pageFriends .pendingRequests .nodata").addClass("hidden"); + const html = result.body.data + .map( + (item) => ` + ${item.initiatorName} + ${format(item.addedAt)} ago + + + + + + ` + ) + .join("\n"); + + $(".pageFriends .pendingRequests tbody").html(html); + } + } + + $(".pageFriends .pendingRequests .loading").addClass("hidden"); +} + +$("#friendAdd").on("click", () => { + addFriendModal.show(undefined, {}); +}); + +$(".pageFriends .pendingRequests button.refresh").on("click", async () => { + void updatePendingRequests(); +}); + +$(".pageFriends .pendingRequests table").on("click", async (e) => { + const action = Array.from(e.target.classList).find((it) => + ["accepted", "rejected", "blocked"].includes(it) + ); + + if (action === undefined) return; + + const id = e.target.parentElement?.parentElement?.dataset["id"]; + if (id === undefined) { + throw new Error("Cannot find id of target."); + } + + if (action === "rejected") { + const result = await Ape.friends.deleteRequest({ + params: { id }, + }); + + if (result.status !== 200) { + Notifications.add( + `Cannot reject friend request: ${result.body.message}`, + -1 + ); + } else { + e.target.parentElement?.parentElement?.remove(); + } + } else { + const result = await Ape.friends.updateRequest({ + params: { id }, + body: { status: action as "accepted" | "blocked" }, + }); + + if (result.status !== 200) { + Notifications.add( + `Cannot update friend request: ${result.body.message}`, + -1 + ); + } else { + e.target.parentElement?.parentElement?.remove(); + } + } +}); + +export const page = new Page({ + id: "friends", + display: "Friends", + element: pageElement, + path: "/friends", + afterHide: async (): Promise => { + Skeleton.remove("pageFriends"); + }, + beforeShow: async (): Promise => { + Skeleton.append("pageFriends", "main"); + + await updatePendingRequests(); + }, +}); + +$(() => { + Skeleton.save("pageFriends"); +}); diff --git a/frontend/src/ts/pages/page.ts b/frontend/src/ts/pages/page.ts index caf112b90..722ee1826 100644 --- a/frontend/src/ts/pages/page.ts +++ b/frontend/src/ts/pages/page.ts @@ -15,7 +15,8 @@ export type PageName = | "profileSearch" | "404" | "accountSettings" - | "leaderboards"; + | "leaderboards" + | "friends"; type Options = { params?: Record; diff --git a/frontend/src/ts/ready.ts b/frontend/src/ts/ready.ts index f4ca1ee82..b55650894 100644 --- a/frontend/src/ts/ready.ts +++ b/frontend/src/ts/ready.ts @@ -36,6 +36,9 @@ $(async (): Promise => { $(".login").addClass("hidden"); $(".disabledNotification").removeClass("hidden"); } + if (!ServerConfiguration.get()?.friends.enabled) { + $(".accountButtonAndMenu .goToFriends").addClass("hidden"); + } }); } MonkeyPower.init();