Skip to content

Commit

Permalink
[frontend] Add online status to user avatar (#214)
Browse files Browse the repository at this point in the history
  • Loading branch information
usatie authored Jan 30, 2024
1 parent 606f420 commit a58a418
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 75 deletions.
7 changes: 7 additions & 0 deletions frontend/app/lib/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -884,3 +884,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)));
10 changes: 8 additions & 2 deletions frontend/app/pong/GameCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<div className="flex gap-4 items-center">
{user.name}
<UserTooltip user={user} avatarSize="medium" />
<Avatar
avatarURL={user.avatarURL}
size="medium"
href={`/user/${user.id}`}
alt={user.name}
online={true}
/>
</div>
</>
);
Expand Down
11 changes: 2 additions & 9 deletions frontend/app/room/[id]/sidebar-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,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,
Expand All @@ -38,13 +38,6 @@ function truncateString(str: string | undefined, num: number): string {
return str.slice(0, num) + "...";
}

function Avatar({ avatarURL }: { avatarURL?: string }) {
if (!avatarURL) {
return <SmallAvatarSkeleton />;
}
return <img className="rounded-full w-6 h-6 object-cover" src={avatarURL} />;
}

export interface LeaveEvent {
userId: number;
roomId: number;
Expand Down Expand Up @@ -141,7 +134,7 @@ export default function SidebarItem({
{!isKicked && (
<ContextMenu>
<ContextMenuTrigger className="flex gap-2 items-center group hover:opacity-60">
<Avatar avatarURL={user.user.avatarURL} />
<Avatar avatarURL={user.user.avatarURL} size="small" />
<span className="text-muted-foreground text-sm whitespace-nowrap group-hover:text-primary">
{truncateString(user.user.name, 15)}
{room.accessLevel !== "DIRECT" && isUserOwner && " 👑"}
Expand Down
7 changes: 5 additions & 2 deletions frontend/app/ui/room/message-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { MessageEvent } from "@/app/lib/dtos";
import { Avatar } from "@/app/ui/user/avatar";
import { Stack } from "@/components/layout/stack";
import { TooltipProvider } from "@radix-ui/react-tooltip";
import UserTooltip from "../user/user-tool-tip";

export function MessageItem({
message,
Expand All @@ -18,7 +17,11 @@ export function MessageItem({
{/* Left Side */}
{withAvatar && (
<TooltipProvider>
<UserTooltip user={message.user} avatarSize="medium"></UserTooltip>
<Avatar
avatarURL={message.user.avatarURL}
size="medium"
href={`/user/${message.user.id}`}
></Avatar>
</TooltipProvider>
)}
{!withAvatar && (
Expand Down
72 changes: 55 additions & 17 deletions frontend/app/ui/user/avatar.tsx
Original file line number Diff line number Diff line change
@@ -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 <Skeleton className={`flex-none rounded-full ${sizeClass}`} />;
} else {
return (
<img
className={`flex-none rounded-full object-cover ${sizeClass}`}
src={avatarURL}
alt={alt}
/>
);
}
const TooltipWrapper = ({ children }: { children: React.ReactNode }) =>
alt !== undefined ? (
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent>{alt}</TooltipContent>
</Tooltip>
) : (
children
);
const LinkWrapper = ({ children }: { children: React.ReactNode }) =>
href !== undefined ? <Link href={href}>{children}</Link> : children;
return (
<LinkWrapper>
<div className={`relative flex-none ${sizeClass}`}>
<TooltipProvider delayDuration={0}>
<TooltipWrapper>
<img
className={`rounded-full object-cover ${sizeClass}`}
src={avatarURL}
alt={alt}
/>
</TooltipWrapper>
<Tooltip>
<TooltipTrigger asChild>
<div
className={`absolute -bottom-[2px] -right-[2px] border-background rounded-full ${onlineStatusClass}`}
></div>
</TooltipTrigger>
<TooltipContent sideOffset={-2}>
{online ? "online" : "offline"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</LinkWrapper>
);
}
22 changes: 12 additions & 10 deletions frontend/app/ui/user/match-history.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -17,17 +16,19 @@ function MatchDetailItem({
: "text-red-500"
: "";
return (
<Link href={`/user/${detail.user.id}`}>
<div className="flex flex-col gap-2">
<div className="flex gap-2 items-center">
<Avatar avatarURL={detail.user.avatarURL} size="medium" />
<div>{detail.user.name}</div>
<div className={`font-bold ${textColor}`}>
{detail.winLose} ({detail.score})
</div>
<div className="flex flex-col gap-2">
<div className="flex gap-2 items-center">
<Avatar
avatarURL={detail.user.avatarURL}
href={`/user/${detail.user.id}`}
size="medium"
/>
<div>{detail.user.name}</div>
<div className={`font-bold ${textColor}`}>
{detail.winLose} ({detail.score})
</div>
</div>
</Link>
</div>
);
}

Expand All @@ -37,6 +38,7 @@ export default async function MatchHistory({ userId }: { userId: number }) {
<ProfileItem title="Match History">
<div className="flex flex-col gap-2">
{history.map((match) => {
// TODO: player2 が undefinedでerrorになる
return (
<div key={match.id} className="flex gap-8 items-center">
<MatchDetailItem
Expand Down
36 changes: 33 additions & 3 deletions frontend/app/ui/user/user-list.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"use client";

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";
import { useEffect, useState } from "react";
import { isOnline } from "@/app/lib/actions";

export default function UserList({
users,
Expand All @@ -10,12 +13,39 @@ export default function UserList({
users: PublicUserEntity[];
avatarSize: AvatarSize;
}) {
const [onlineStatus, setOnlineStatus] = useState<{ [key: string]: boolean }>(
{},
);

const fetchOnlineStatus = async () => {
try {
users.forEach(async (u) => {
const body = await isOnline(u.id);
const online = body.isOnline;
setOnlineStatus((prev) => ({ ...prev, [u.name]: online }));
});
} catch (error) {
console.error("Error fetching online status:", error);
}
};

useEffect(() => {
fetchOnlineStatus();
}, []);

return (
<TooltipProvider delayDuration={0}>
<div className="flex flex-wrap gap-2">
{users.length === 0 && <div>No users to display</div>}
{users.map((u) => (
<UserTooltip key={u.id} user={u} avatarSize={avatarSize} />
<Avatar
avatarURL={u.avatarURL}
size={avatarSize}
href={`/user/${u.id}`}
alt={u.name}
online={onlineStatus[u.name]}
key={u.id}
/>
))}
</div>
</TooltipProvider>
Expand Down
32 changes: 0 additions & 32 deletions frontend/app/ui/user/user-tool-tip.tsx

This file was deleted.

0 comments on commit a58a418

Please sign in to comment.