Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[frontend] Add online status to user avatar #214

Merged
merged 13 commits into from
Jan 30, 2024
Merged
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.

Loading