diff --git a/frontend/src/resources/css/_chat.scss b/frontend/src/resources/css/_chat.scss index 479e739..42f54e7 100644 --- a/frontend/src/resources/css/_chat.scss +++ b/frontend/src/resources/css/_chat.scss @@ -340,3 +340,25 @@ } } +.message-profile-pic { + img { + width: 40px; + height: 40px; + border-radius: 50%; + object-fit: cover; + border: 2px solid $color-dark-outline; + + &.loading { + opacity: 0.6; + cursor: default; + } + } +} + +.message-username { + &.loading { + opacity: 0.6; + cursor: default; + } +} + diff --git a/frontend/src/resources/css/_profile.scss b/frontend/src/resources/css/_profile.scss index 518c983..f3f60a4 100644 --- a/frontend/src/resources/css/_profile.scss +++ b/frontend/src/resources/css/_profile.scss @@ -187,6 +187,16 @@ } } } + + .profile-actions { + display: flex; + gap: 0.75rem; + margin-top: 0.5rem; + + mdui-button { + flex: 1; + } + } } } diff --git a/frontend/src/ui/api/profileApi.ts b/frontend/src/ui/api/profileApi.ts index a60f820..32a4b61 100644 --- a/frontend/src/ui/api/profileApi.ts +++ b/frontend/src/ui/api/profileApi.ts @@ -1,5 +1,6 @@ import { getAuthHeaders } from "../../auth/api"; import { API_BASE_URL } from "../../core/config"; +import type { UserProfile } from "../../core/types"; export interface ProfileData { profile_picture?: string; @@ -90,3 +91,23 @@ export async function updateBio(token: string, bio: string): Promise { return false; } } + +/** + * Fetches user profile data by username + */ +export async function fetchUserProfile(token: string, username: string): Promise { + try { + const response = await fetch(`${API_BASE_URL}/user/${username}`, { + headers: getAuthHeaders(token) + }); + + if (response.ok) { + return await response.json(); + } + + return null; + } catch (error) { + console.error('Error fetching user profile:', error); + return null; + } +} diff --git a/frontend/src/ui/components/chat/ChatMessages.tsx b/frontend/src/ui/components/chat/ChatMessages.tsx index 16e773a..cee6d0d 100644 --- a/frontend/src/ui/components/chat/ChatMessages.tsx +++ b/frontend/src/ui/components/chat/ChatMessages.tsx @@ -2,14 +2,34 @@ import { useChat } from "../../hooks/useChat"; import { Message } from "./Message"; import { useAppState } from "../../state"; import type { Message as MessageType } from "../../../core/types"; +import type { UserProfile } from "../../../core/types"; +import { UserProfileDialog } from "./UserProfileDialog"; +import { fetchUserProfile } from "../../api/profileApi"; +import { useState } from "react"; +import { delay } from "../../../utils/utils"; export function ChatMessages() { const { messages } = useChat(); const { user } = useAppState(); + const [profileDialogOpen, setProfileDialogOpen] = useState(false); + const [selectedUserProfile, setSelectedUserProfile] = useState(null); + const [isLoadingProfile, setIsLoadingProfile] = useState(false); - const handleProfileClick = (username: string) => { - // TODO: Show user profile dialog - console.log("Show profile for:", username); + const handleProfileClick = async (username: string) => { + if (!user.authToken) return; + + setIsLoadingProfile(true); + try { + const profile = await fetchUserProfile(user.authToken, username); + if (profile) { + setSelectedUserProfile(profile); + setProfileDialogOpen(true); + } + } catch (error) { + console.error("Failed to fetch user profile:", error); + } finally { + setIsLoadingProfile(false); + } }; const handleContextMenu = (e: React.MouseEvent, message: MessageType) => { @@ -19,16 +39,31 @@ export function ChatMessages() { }; return ( -
- {messages.map((message) => ( - - ))} -
+ <> +
+ {messages.map((message) => ( + + ))} +
+ + { + setProfileDialogOpen(value); + if (!value) { + await delay(1000); + setSelectedUserProfile(null); + } + }} + userProfile={selectedUserProfile} + /> + ); } diff --git a/frontend/src/ui/components/chat/Message.tsx b/frontend/src/ui/components/chat/Message.tsx index 8e44817..6817f2a 100644 --- a/frontend/src/ui/components/chat/Message.tsx +++ b/frontend/src/ui/components/chat/Message.tsx @@ -7,9 +7,10 @@ interface MessageProps { isAuthor: boolean; onProfileClick: (username: string) => void; onContextMenu: (e: React.MouseEvent, message: MessageType) => void; + isLoadingProfile?: boolean; } -export function Message({ message, isAuthor, onProfileClick, onContextMenu }: MessageProps) { +export function Message({ message, isAuthor, onProfileClick, onContextMenu, isLoadingProfile = false }: MessageProps) { return (
onProfileClick(message.username)} - style={{ cursor: "pointer" }} + onClick={() => !isLoadingProfile && onProfileClick(message.username)} + style={{ cursor: isLoadingProfile ? "default" : "pointer" }} + className={isLoadingProfile ? "loading" : ""} onError={(e) => { const target = e.target as HTMLImageElement; target.src = defaultAvatar; @@ -35,9 +37,9 @@ export function Message({ message, isAuthor, onProfileClick, onContextMenu }: Me {!isAuthor && (
onProfileClick(message.username)} - style={{ cursor: "pointer" }}> {/* TODO extract to SCSS */} + className={`message-username ${isLoadingProfile ? "loading" : ""}`} + onClick={() => !isLoadingProfile && onProfileClick(message.username)} + style={{ cursor: isLoadingProfile ? "default" : "pointer" }}> {/* TODO extract to SCSS */} {message.username}
)} diff --git a/frontend/src/ui/components/chat/UserProfileDialog.tsx b/frontend/src/ui/components/chat/UserProfileDialog.tsx index 70291ba..b3cbdc9 100644 --- a/frontend/src/ui/components/chat/UserProfileDialog.tsx +++ b/frontend/src/ui/components/chat/UserProfileDialog.tsx @@ -1,40 +1,71 @@ import type { DialogProps } from "../../../core/types"; +import type { UserProfile } from "../../../core/types"; import { MaterialDialog } from "../Dialog"; +import { formatTime } from "../../../utils/utils"; +import defaultAvatar from "../../../resources/images/default-avatar.png"; -export function UserProfileDialog({ isOpen, onOpenChange }: DialogProps) { - return ( - -
-
- Profile Picture +interface UserProfileDialogProps extends DialogProps { + userProfile: UserProfile | null; +} + +export function UserProfileDialog({ isOpen, onOpenChange, userProfile }: UserProfileDialogProps) { + const content = userProfile ? ( +
+
+ Profile Picture { + const target = e.target as HTMLImageElement; + target.src = defaultAvatar; + }} + /> +
+
+
+

{userProfile.username}

+
+ {userProfile.online ? ( + <> + Online + + ) : ( + <> + Last seen {formatTime(userProfile.last_seen)} + + )} +
-
-
-

-
+
+ +
+ {userProfile.bio || "No bio available."}
-
- -
+
+
+
+ Member since: + {formatTime(userProfile.created_at)}
-
-
- Member since: - -
-
- Last seen: - -
-
-
- - - Send Message - +
+ Last seen: + {formatTime(userProfile.last_seen)}
+
+ + + Send Message + +
+
+ ) : null + + return ( + + {content} ); }