From 6c701a42c92be167834bdf16946100d92e6c5ce3 Mon Sep 17 00:00:00 2001 From: Shun Usami Date: Thu, 18 Jan 2024 00:03:41 +0900 Subject: [PATCH 1/5] [frontend] Add online status to user avatar --- frontend/app/room/[id]/sidebar-item.tsx | 11 +--- frontend/app/ui/user/avatar.tsx | 72 +++++++++++++++++++------ frontend/app/ui/user/match-history.tsx | 21 ++++---- frontend/app/ui/user/user-list.tsx | 12 +++-- frontend/app/ui/user/user-tool-tip.tsx | 32 ----------- 5 files changed, 77 insertions(+), 71 deletions(-) delete mode 100644 frontend/app/ui/user/user-tool-tip.tsx diff --git a/frontend/app/room/[id]/sidebar-item.tsx b/frontend/app/room/[id]/sidebar-item.tsx index ad40bffc..af9c7709 100644 --- a/frontend/app/room/[id]/sidebar-item.tsx +++ b/frontend/app/room/[id]/sidebar-item.tsx @@ -11,7 +11,7 @@ import type { RoomEntity, UserOnRoomEntity, } from "@/app/lib/dtos"; -import { SmallAvatarSkeleton } from "@/app/ui/room/skeleton"; +import { Avatar } from "@/app/ui/user/avatar"; import { ContextMenu, ContextMenuContent, @@ -33,13 +33,6 @@ function truncateString(str: string | undefined, num: number): string { return str.slice(0, num) + "..."; } -function Avatar({ avatarURL }: { avatarURL?: string }) { - if (!avatarURL) { - return ; - } - return ; -} - export interface LeaveEvent { userId: number; roomId: number; @@ -110,7 +103,7 @@ export default function SidebarItem({ {!isKicked && ( - + {truncateString(user.user.name, 15)} {room.accessLevel !== "DIRECT" && isUserOwner && " 👑"} diff --git a/frontend/app/ui/user/avatar.tsx b/frontend/app/ui/user/avatar.tsx index 2e9e99e3..2e71db43 100644 --- a/frontend/app/ui/user/avatar.tsx +++ b/frontend/app/ui/user/avatar.tsx @@ -1,40 +1,78 @@ import { Skeleton } from "@/components/ui/skeleton"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import Link from "next/link"; export type AvatarSize = "small" | "medium" | "large"; -export function Avatar({ - avatarURL, - size, - alt, -}: { +export interface Props { avatarURL?: string; size: AvatarSize; + href?: string; alt?: string; -}) { + online?: boolean; +} + +export function Avatar({ avatarURL, size, href, alt, online }: Props) { let sizeClass = ""; + let onlineStatusClass = online ? "bg-green-500 " : "bg-gray-500 "; switch (size) { case "small": sizeClass = "h-6 w-6"; + onlineStatusClass += "w-3 h-3 border-2"; break; case "medium": sizeClass = "h-10 w-10"; + onlineStatusClass += "w-4 h-4 border-2"; break; case "large": - sizeClass = "h-32 w-32"; + sizeClass = "h-28 w-28"; + onlineStatusClass += "w-8 h-8 border-4"; break; default: - sizeClass = "h-10 w-10"; - break; + throw new Error("Invalid size"); } if (!avatarURL) { return ; - } else { - return ( - {alt} - ); } + const TooltipWrapper = ({ children }: { children: React.ReactNode }) => + alt !== undefined ? ( + + {children} + {alt} + + ) : ( + children + ); + const LinkWrapper = ({ children }: { children: React.ReactNode }) => + href !== undefined ? {children} : children; + return ( + +
+ + + {alt} + + + +
+
+ + {online ? "online" : "offline"} + +
+
+
+
+ ); } diff --git a/frontend/app/ui/user/match-history.tsx b/frontend/app/ui/user/match-history.tsx index 77972a4c..6ba06347 100644 --- a/frontend/app/ui/user/match-history.tsx +++ b/frontend/app/ui/user/match-history.tsx @@ -1,6 +1,5 @@ import { getMatchHistory } from "@/app/lib/actions"; import type { MatchDetailEntity } from "@/app/lib/dtos"; -import Link from "next/link"; import { Avatar } from "./avatar"; import ProfileItem from "./profile-item"; @@ -17,17 +16,19 @@ function MatchDetailItem({ : "text-red-500" : ""; return ( - -
-
- -
{detail.user.name}
-
- {detail.winLose} ({detail.score}) -
+
+
+ +
{detail.user.name}
+
+ {detail.winLose} ({detail.score})
- +
); } diff --git a/frontend/app/ui/user/user-list.tsx b/frontend/app/ui/user/user-list.tsx index bd9c3d33..cfb2191f 100644 --- a/frontend/app/ui/user/user-list.tsx +++ b/frontend/app/ui/user/user-list.tsx @@ -1,7 +1,6 @@ import type { PublicUserEntity } from "@/app/lib/dtos"; import { TooltipProvider } from "@/components/ui/tooltip"; -import { AvatarSize } from "./avatar"; -import UserTooltip from "./user-tool-tip"; +import { Avatar, AvatarSize } from "./avatar"; export default function UserList({ users, @@ -15,7 +14,14 @@ export default function UserList({
{users.length === 0 &&
No users to display
} {users.map((u) => ( - + ))}
diff --git a/frontend/app/ui/user/user-tool-tip.tsx b/frontend/app/ui/user/user-tool-tip.tsx deleted file mode 100644 index 76f76ecd..00000000 --- a/frontend/app/ui/user/user-tool-tip.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import type { PublicUserEntity } from "@/app/lib/dtos"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import Link from "next/link"; -import { Avatar, AvatarSize } from "./avatar"; - -// This component has to be used with TooltipProvider -export default function UserTooltip({ - user, - avatarSize, -}: { - user: PublicUserEntity; - avatarSize: AvatarSize; -}) { - return ( - - - - - - - {user.name} - - ); -} From 89339567ced80421a5829beddcc9c1006a18c953 Mon Sep 17 00:00:00 2001 From: Shun Usami Date: Thu, 18 Jan 2024 00:38:40 +0900 Subject: [PATCH 2/5] [frontend] Replace UserTooltip with Avatar --- frontend/app/pong/GameCard.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/app/pong/GameCard.tsx b/frontend/app/pong/GameCard.tsx index 74f111ae..242cf7d4 100644 --- a/frontend/app/pong/GameCard.tsx +++ b/frontend/app/pong/GameCard.tsx @@ -1,18 +1,24 @@ "use client"; +import { Avatar } from "@/app/ui/user/avatar"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { TooltipProvider } from "@radix-ui/react-tooltip"; import { useRouter } from "next/navigation"; import { PublicUserEntity } from "../lib/dtos"; -import UserTooltip from "../ui/user/user-tool-tip"; function UserCard({ user }: { user: PublicUserEntity }) { return ( <>
{user.name} - +
); From 11ac5a48f37b12e90f846d8becae300c1ff1953f Mon Sep 17 00:00:00 2001 From: kotto5 Date: Tue, 30 Jan 2024 12:12:41 +0900 Subject: [PATCH 3/5] [frontend] Add isOnline function to check user's online status --- frontend/app/lib/actions.ts | 7 +++++++ frontend/app/ui/user/user-list.tsx | 28 +++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/frontend/app/lib/actions.ts b/frontend/app/lib/actions.ts index 6324dea8..f2b64115 100644 --- a/frontend/app/lib/actions.ts +++ b/frontend/app/lib/actions.ts @@ -824,3 +824,10 @@ export async function createUserWithOauth( redirect("/login"); } } + +export const isOnline = async (userId: number) => + fetch(`${process.env.API_URL}/chat/${userId}/online`, { + headers: { + Authorization: "Bearer " + getAccessToken(), + }, + }).then((res) => (res.ok ? res.json() : Promise.reject(res))); diff --git a/frontend/app/ui/user/user-list.tsx b/frontend/app/ui/user/user-list.tsx index cfb2191f..18295fc5 100644 --- a/frontend/app/ui/user/user-list.tsx +++ b/frontend/app/ui/user/user-list.tsx @@ -1,6 +1,13 @@ +"use client"; + import type { PublicUserEntity } from "@/app/lib/dtos"; import { TooltipProvider } from "@/components/ui/tooltip"; import { Avatar, AvatarSize } from "./avatar"; +<<<<<<< Updated upstream +======= +import { useEffect, useState } from "react"; +import { isOnline } from "@/app/lib/actions"; +>>>>>>> Stashed changes export default function UserList({ users, @@ -9,6 +16,25 @@ export default function UserList({ users: PublicUserEntity[]; avatarSize: AvatarSize; }) { + const [onlineStatus, setOnlineStatus] = useState<{ [key: string]: boolean }>( + {} + ); + + const fetchOnlineStatus = async () => { + try { + users.forEach(async (u) => { + const online = await isOnline(u.id); + setOnlineStatus((prev) => ({ ...prev, [u.name]: online })); + }); + } catch (error) { + console.error("Error fetching online status:", error); + } + }; + + useEffect(() => { + fetchOnlineStatus(); + }, []); + return (
@@ -19,7 +45,7 @@ export default function UserList({ size={avatarSize} href={`/user/${u.id}`} alt={u.name} - online={u.name === "Susami"} + online={onlineStatus[u.name]} key={u.id} /> ))} From 9a5b26ca2c60a66b40db6a44b17ec9f41dbf0efb Mon Sep 17 00:00:00 2001 From: kotto5 Date: Tue, 30 Jan 2024 12:12:55 +0900 Subject: [PATCH 4/5] [frontend] make fmt --- frontend/app/lib/actions.ts | 42 ++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/frontend/app/lib/actions.ts b/frontend/app/lib/actions.ts index f2b64115..3797413a 100644 --- a/frontend/app/lib/actions.ts +++ b/frontend/app/lib/actions.ts @@ -48,7 +48,7 @@ export async function signIn({ export async function authenticate( prevState: string | undefined, - formData: FormData, + formData: FormData ) { try { await signIn({ @@ -98,7 +98,7 @@ export async function getUser(id: number) { export async function updateUser( prevState: string | undefined, - formData: FormData, + formData: FormData ) { const { user_id, ...updateData } = Object.fromEntries(formData.entries()); const res = await fetch(`${process.env.API_URL}/user/${user_id}`, { @@ -121,7 +121,7 @@ export async function updateUser( export async function deleteUser( prevState: string | undefined, - formData: FormData, + formData: FormData ) { const { user_id } = Object.fromEntries(formData.entries()); const res = await fetch(`${process.env.API_URL}/user/${user_id}`, { @@ -194,7 +194,7 @@ export async function getDirectRoom(userId: number) { export async function createRoom( prevState: { error?: string }, - formData: FormData, + formData: FormData ) { let payload; if (formData.get("accessLevel") === "PROTECTED") { @@ -256,7 +256,7 @@ export async function createDirectRoom(userId: number) { export async function joinRoom( roomId: number, prevState: { error: string } | undefined, - formData: FormData, + formData: FormData ) { const payload = JSON.stringify({ password: formData.get("password"), @@ -288,7 +288,7 @@ export async function inviteUserToRoom(roomId: number, userId: number) { headers: { Authorization: "Bearer " + getAccessToken(), }, - }, + } ); const data = await res.json(); if (!res.ok) { @@ -303,7 +303,7 @@ export async function updateRoom( roomName: string, roomId: number, accessLevel: AccessLevel, - password?: string, + password?: string ) { const res = await fetch(`${process.env.API_URL}/room/${roomId}`, { method: "PATCH", @@ -324,7 +324,7 @@ export async function updateRoom( export async function updateRoomUser( role: string, roomId: number, - userId: number, + userId: number ) { const res = await fetch(`${process.env.API_URL}/room/${roomId}/${userId}`, { method: "PATCH", @@ -353,7 +353,7 @@ export async function kickUserOnRoom(roomId: number, userId: number) { headers: { Authorization: "Bearer " + getAccessToken(), }, - }, + } ); if (!res.ok) { console.error("kickUserOnRoom error: ", await res.json()); @@ -405,7 +405,7 @@ export async function getMessages(roomId: number) { export async function updatePassword( prevState: string | undefined, - formData: FormData, + formData: FormData ) { // Check if new password and confirm password match const newPassword = formData.get("new-password"); @@ -528,7 +528,7 @@ export async function addFriend(recipientId: number) { headers: { Authorization: "Bearer " + getAccessToken(), }, - }, + } ); if (!res.ok) { console.error("addFriend error: ", await res.json()); @@ -547,7 +547,7 @@ export async function getFriendRequests(): Promise { headers: { Authorization: "Bearer " + getAccessToken(), }, - }, + } ); if (!res.ok) { console.error("getFriendRequests error: ", await res.json()); @@ -567,7 +567,7 @@ export async function acceptFriendRequest(requesterId: number) { headers: { Authorization: "Bearer " + getAccessToken(), }, - }, + } ); if (!res.ok) { console.error("acceptFriendRequest error: ", await res.json()); @@ -587,7 +587,7 @@ export async function rejectFriendRequest(requesterId: number) { headers: { Authorization: "Bearer " + getAccessToken(), }, - }, + } ); if (!res.ok) { console.error("rejectFriendRequest error: ", await res.json()); @@ -607,7 +607,7 @@ export async function cancelFriendRequest(recipientId: number) { headers: { Authorization: "Bearer " + getAccessToken(), }, - }, + } ); if (!res.ok) { console.error("cancelFriendRequest error: ", await res.json()); @@ -638,7 +638,7 @@ export async function unfriend(friendId: number) { } export async function getMatchHistory( - userId: number, + userId: number ): Promise { console.log(`${process.env.API_URL}/user/${userId}/history`); const res = await fetch(`${process.env.API_URL}/user/${userId}/history`, { @@ -688,7 +688,7 @@ export async function generate2FASecret() { export async function enableTwoFactorAuthentication( prevState: string, - formData: FormData, + formData: FormData ) { const code = formData.get("code") as string; const res = await fetch(`${process.env.API_URL}/auth/2fa/enable`, { @@ -712,7 +712,7 @@ export async function enableTwoFactorAuthentication( export async function twoFactorAuthenticate( prevState: string, - formData: FormData, + formData: FormData ) { const code = formData.get("code") as string; const res = await fetch(`${process.env.API_URL}/auth/2fa/authenticate`, { @@ -742,7 +742,7 @@ export async function banUser(roomId: number, userId: number) { headers: { Authorization: "Bearer " + getAccessToken(), }, - }, + } ); if (!res.ok) { console.error("banUser error: ", await res.json()); @@ -776,7 +776,7 @@ export async function unbanUser(roomId: number, userId: number) { headers: { Authorization: "Bearer " + getAccessToken(), }, - }, + } ); if (!res.ok) { console.error("unbanUser error: ", await res.json()); @@ -805,7 +805,7 @@ export async function leaveRoom(roomId: number) { export async function createUserWithOauth( code: string | string[] | undefined, - provider: string, + provider: string ) { if (!code) return; const url = `${process.env.API_URL}/auth/oauth2/signup/${provider}`; From a8257935ce064eef390b3e34764b3d0a0f92ba48 Mon Sep 17 00:00:00 2001 From: kotto5 Date: Tue, 30 Jan 2024 12:14:33 +0900 Subject: [PATCH 5/5] [frontend] Remove unused import and fix formatting in user-list.tsx --- frontend/app/ui/user/user-list.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/app/ui/user/user-list.tsx b/frontend/app/ui/user/user-list.tsx index 18295fc5..be15a13a 100644 --- a/frontend/app/ui/user/user-list.tsx +++ b/frontend/app/ui/user/user-list.tsx @@ -3,11 +3,8 @@ import type { PublicUserEntity } from "@/app/lib/dtos"; import { TooltipProvider } from "@/components/ui/tooltip"; import { Avatar, AvatarSize } from "./avatar"; -<<<<<<< Updated upstream -======= import { useEffect, useState } from "react"; import { isOnline } from "@/app/lib/actions"; ->>>>>>> Stashed changes export default function UserList({ users,