feat: add friend requests frontend (@fehmer)

This commit is contained in:
Christian Fehmer 2025-07-07 12:47:20 +02:00 committed by Christian Fehmer
parent 7eda87cfd3
commit 837051f386
11 changed files with 283 additions and 3 deletions

View file

@ -175,6 +175,16 @@
<i class="fas fa-fw fa-cog"></i>
Account settings
</a>
<!-- TODO: disable if configuration is not set for friends -->
<a
href="/friends"
class="button goToFriends"
onclick="this.blur();"
router-link
>
<i class="fas fa-fw fa-user-friends"></i>
Friends
</a>
<button class="signOut" onclick="this.blur();">
<i class="fas fa-fw fa-sign-out-alt"></i>
Sign out

View file

@ -0,0 +1,92 @@
<div class="page pageFriends hidden" id="pageFriends">
<div class="preloader hidden">
<div class="icon">
<i class="fas fa-fw fa-spin fa-circle-notch"></i>
</div>
<div class="barWrapper hidden">
<div class="bar">
<div class="fill"></div>
</div>
<div class="text"></div>
</div>
</div>
<div class="content">
<div class="request">
<div class="buttons">
<button
id="friendAdd"
class="button"
aria-label="add friend"
data-balloon-pos="right"
>
<i class="fas fa-user-plus fa-fw"></i>
</button>
</div>
</div>
<div class="pendingRequests">
<div class="bigtitle">
<div class="text">
Pending Friend Requests
<button
class="refresh"
aria-label="refresh"
data-balloon-pos="bottom"
>
<i class="fas fa-sync fa-fw"></i>
</button>
</div>
</div>
<div class="loading hidden">
<i class="fas fa-circle-notch fa-spin"></i>
</div>
<div class="error hidden">
<i class="fas fa-times"></i>
<p>Something went wrong</p>
</div>
<div class="nodata hidden">No pending friend requests :)</div>
<table>
<thead>
<tr>
<td>user</td>
<td>date</td>
<td>actions</td>
</tr>
</thead>
<tbody>
<tr data-id="id">
<td>Username</td>
<td class="small">3 days ago</td>
<td>
<button
class="accept"
aria-label="accept friend"
data-balloon-pos="bottom"
>
<i class="fas fa-user-check fa-fw"></i>
</button>
</td>
<td>
<button
class="reject"
aria-label="reject friend"
data-balloon-pos="bottom"
>
<i class="fas fa-user-times fa-fw"></i>
</button>
</td>
<td>
<button
class="block"
aria-label="block user from sending friend requests"
data-balloon-pos="bottom"
>
<i class="fas fa-user-slash fa-fw"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View file

@ -50,6 +50,7 @@
<load src="html/pages/test.html" />
<load src="html/pages/404.html" />
<load src="html/pages/account-settings.html" />
<load src="html/pages/friends.html" />
<load src="html/pages/leaderboards.html" />
</main>
<load src="html/footer.html" />

View file

@ -0,0 +1,2 @@
.pageFriends {
}

View file

@ -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";

View file

@ -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,

View file

@ -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(

View file

@ -1318,7 +1318,7 @@ ConfigEvent.subscribe((eventKey) => {
}
});
export const page = new Page({
export const page = new Page<undefined>({
id: "account",
element: $(".page.pageAccount"),
path: "/account",

View file

@ -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<void> {
$(".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) => `<tr data-id="${item._id}">
<td>${item.initiatorName}</td>
<td>${format(item.addedAt)} ago</td>
<td class="actions">
<button class="accepted" aria-label="accept friend" data-balloon-pos="bottom">
<i class="fas fa-user-check fa-fw"></i>
</button>
<button class="rejected" aria-label="reject friend" data-balloon-pos="bottom">
<i class="fas fa-user-times fa-fw"></i>
</button>
<button class="blocked" aria-label="block user from sending friend requests" data-balloon-pos="bottom">
<i class="fas fa-user-slash fa-fw"></i>
</button>
</td>
</tr>`
)
.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<undefined>({
id: "friends",
display: "Friends",
element: pageElement,
path: "/friends",
afterHide: async (): Promise<void> => {
Skeleton.remove("pageFriends");
},
beforeShow: async (): Promise<void> => {
Skeleton.append("pageFriends", "main");
await updatePendingRequests();
},
});
$(() => {
Skeleton.save("pageFriends");
});

View file

@ -15,7 +15,8 @@ export type PageName =
| "profileSearch"
| "404"
| "accountSettings"
| "leaderboards";
| "leaderboards"
| "friends";
type Options<T> = {
params?: Record<string, string>;

View file

@ -36,6 +36,9 @@ $(async (): Promise<void> => {
$(".login").addClass("hidden");
$(".disabledNotification").removeClass("hidden");
}
if (!ServerConfiguration.get()?.friends.enabled) {
$(".accountButtonAndMenu .goToFriends").addClass("hidden");
}
});
}
MonkeyPower.init();