mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-11-09 21:51:29 +08:00
feat: add friend requests frontend (@fehmer)
This commit is contained in:
parent
7eda87cfd3
commit
837051f386
11 changed files with 283 additions and 3 deletions
|
|
@ -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
|
||||
|
|
|
|||
92
frontend/src/html/pages/friends.html
Normal file
92
frontend/src/html/pages/friends.html
Normal 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>
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
2
frontend/src/styles/friends.scss
Normal file
2
frontend/src/styles/friends.scss
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
.pageFriends {
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
150
frontend/src/ts/pages/friends.ts
Normal file
150
frontend/src/ts/pages/friends.ts
Normal 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");
|
||||
});
|
||||
|
|
@ -15,7 +15,8 @@ export type PageName =
|
|||
| "profileSearch"
|
||||
| "404"
|
||||
| "accountSettings"
|
||||
| "leaderboards";
|
||||
| "leaderboards"
|
||||
| "friends";
|
||||
|
||||
type Options<T> = {
|
||||
params?: Record<string, string>;
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ $(async (): Promise<void> => {
|
|||
$(".login").addClass("hidden");
|
||||
$(".disabledNotification").removeClass("hidden");
|
||||
}
|
||||
if (!ServerConfiguration.get()?.friends.enabled) {
|
||||
$(".accountButtonAndMenu .goToFriends").addClass("hidden");
|
||||
}
|
||||
});
|
||||
}
|
||||
MonkeyPower.init();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue