Fix user profile dialog

This commit is contained in:
2025-09-03 22:47:03 +03:00
Unverified
parent 7796e9577e
commit 182fe88db4
6 changed files with 169 additions and 48 deletions
+22
View File
@@ -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;
}
}
+10
View File
@@ -187,6 +187,16 @@
} }
} }
} }
.profile-actions {
display: flex;
gap: 0.75rem;
margin-top: 0.5rem;
mdui-button {
flex: 1;
}
}
} }
} }
+21
View File
@@ -1,5 +1,6 @@
import { getAuthHeaders } from "../../auth/api"; import { getAuthHeaders } from "../../auth/api";
import { API_BASE_URL } from "../../core/config"; import { API_BASE_URL } from "../../core/config";
import type { UserProfile } from "../../core/types";
export interface ProfileData { export interface ProfileData {
profile_picture?: string; profile_picture?: string;
@@ -90,3 +91,23 @@ export async function updateBio(token: string, bio: string): Promise<boolean> {
return false; return false;
} }
} }
/**
* Fetches user profile data by username
*/
export async function fetchUserProfile(token: string, username: string): Promise<UserProfile | null> {
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;
}
}
@@ -2,14 +2,34 @@ import { useChat } from "../../hooks/useChat";
import { Message } from "./Message"; import { Message } from "./Message";
import { useAppState } from "../../state"; import { useAppState } from "../../state";
import type { Message as MessageType } from "../../../core/types"; 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() { export function ChatMessages() {
const { messages } = useChat(); const { messages } = useChat();
const { user } = useAppState(); const { user } = useAppState();
const [profileDialogOpen, setProfileDialogOpen] = useState(false);
const [selectedUserProfile, setSelectedUserProfile] = useState<UserProfile | null>(null);
const [isLoadingProfile, setIsLoadingProfile] = useState(false);
const handleProfileClick = (username: string) => { const handleProfileClick = async (username: string) => {
// TODO: Show user profile dialog if (!user.authToken) return;
console.log("Show profile for:", username);
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) => { const handleContextMenu = (e: React.MouseEvent, message: MessageType) => {
@@ -19,16 +39,31 @@ export function ChatMessages() {
}; };
return ( return (
<div className="chat-messages" id="chat-messages"> <>
{messages.map((message) => ( <div className="chat-messages" id="chat-messages">
<Message {messages.map((message) => (
key={message.id} <Message
message={message} key={message.id}
isAuthor={message.username === user.currentUser?.username} message={message}
onProfileClick={handleProfileClick} isAuthor={message.username === user.currentUser?.username}
onContextMenu={handleContextMenu} onProfileClick={handleProfileClick}
/> onContextMenu={handleContextMenu}
))} isLoadingProfile={isLoadingProfile}
</div> />
))}
</div>
<UserProfileDialog
isOpen={profileDialogOpen}
onOpenChange={async (value) => {
setProfileDialogOpen(value);
if (!value) {
await delay(1000);
setSelectedUserProfile(null);
}
}}
userProfile={selectedUserProfile}
/>
</>
); );
} }
+8 -6
View File
@@ -7,9 +7,10 @@ interface MessageProps {
isAuthor: boolean; isAuthor: boolean;
onProfileClick: (username: string) => void; onProfileClick: (username: string) => void;
onContextMenu: (e: React.MouseEvent, message: MessageType) => 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 ( return (
<div <div
className={`message ${isAuthor ? "sent" : "received"}`} className={`message ${isAuthor ? "sent" : "received"}`}
@@ -23,8 +24,9 @@ export function Message({ message, isAuthor, onProfileClick, onContextMenu }: Me
<img <img
src={message.profile_picture || defaultAvatar} src={message.profile_picture || defaultAvatar}
alt={message.username} alt={message.username}
onClick={() => onProfileClick(message.username)} onClick={() => !isLoadingProfile && onProfileClick(message.username)}
style={{ cursor: "pointer" }} style={{ cursor: isLoadingProfile ? "default" : "pointer" }}
className={isLoadingProfile ? "loading" : ""}
onError={(e) => { onError={(e) => {
const target = e.target as HTMLImageElement; const target = e.target as HTMLImageElement;
target.src = defaultAvatar; target.src = defaultAvatar;
@@ -35,9 +37,9 @@ export function Message({ message, isAuthor, onProfileClick, onContextMenu }: Me
{!isAuthor && ( {!isAuthor && (
<div <div
className="message-username" className={`message-username ${isLoadingProfile ? "loading" : ""}`}
onClick={() => onProfileClick(message.username)} onClick={() => !isLoadingProfile && onProfileClick(message.username)}
style={{ cursor: "pointer" }}> {/* TODO extract to SCSS */} style={{ cursor: isLoadingProfile ? "default" : "pointer" }}> {/* TODO extract to SCSS */}
{message.username} {message.username}
</div> </div>
)} )}
@@ -1,40 +1,71 @@
import type { DialogProps } from "../../../core/types"; import type { DialogProps } from "../../../core/types";
import type { UserProfile } from "../../../core/types";
import { MaterialDialog } from "../Dialog"; import { MaterialDialog } from "../Dialog";
import { formatTime } from "../../../utils/utils";
import defaultAvatar from "../../../resources/images/default-avatar.png";
export function UserProfileDialog({ isOpen, onOpenChange }: DialogProps) { interface UserProfileDialogProps extends DialogProps {
return ( userProfile: UserProfile | null;
<MaterialDialog open={isOpen} onOpenChange={onOpenChange} close-on-overlay-click close-on-esc> }
<div className="content">
<div className="profile-picture-section"> export function UserProfileDialog({ isOpen, onOpenChange, userProfile }: UserProfileDialogProps) {
<img className="profile-picture" alt="Profile Picture" /> const content = userProfile ? (
<div className="content">
<div className="profile-picture-section">
<img
className="profile-picture"
alt="Profile Picture"
src={userProfile.profile_picture || defaultAvatar}
onError={(e) => {
const target = e.target as HTMLImageElement;
target.src = defaultAvatar;
}}
/>
</div>
<div className="profile-info">
<div className="username-section">
<h4 className="username">{userProfile.username}</h4>
<div className={`online-status ${userProfile.online ? "online" : "offline"}`}>
{userProfile.online ? (
<>
<span className="online-indicator"></span> Online
</>
) : (
<>
<span className="offline-indicator"></span> Last seen {formatTime(userProfile.last_seen)}
</>
)}
</div>
</div> </div>
<div className="profile-info"> <div className="bio-section">
<div className="username-section"> <label>Bio:</label>
<h4 className="username"></h4> <div className="bio-display">
<div className="online-status"></div> {userProfile.bio || "No bio available."}
</div> </div>
<div className="bio-section"> </div>
<label>Bio:</label> <div className="profile-stats">
<div className="bio-display"></div> <div className="stat">
<span className="stat-label">Member since:</span>
<span className="stat-value member-since">{formatTime(userProfile.created_at)}</span>
</div> </div>
<div className="profile-stats"> <div className="stat">
<div className="stat"> <span className="stat-label">Last seen:</span>
<span className="stat-label">Member since:</span> <span className="stat-value last-seen">{formatTime(userProfile.last_seen)}</span>
<span className="stat-value member-since"></span>
</div>
<div className="stat">
<span className="stat-label">Last seen:</span>
<span className="stat-value last-seen"></span>
</div>
</div>
<div className="profile-actions">
<mdui-button id="dm-button" variant="filled">
<mdui-icon slot="icon" name="chat--filled"></mdui-icon>
Send Message
</mdui-button>
</div> </div>
</div> </div>
<div className="profile-actions">
<mdui-button id="dm-button" variant="filled">
<mdui-icon slot="icon" name="chat--filled"></mdui-icon>
Send Message
</mdui-button>
</div>
</div> </div>
</div>
) : null
return (
<MaterialDialog open={isOpen} onOpenChange={onOpenChange} close-on-overlay-click close-on-esc id="user-profile-dialog">
{content}
</MaterialDialog> </MaterialDialog>
); );
} }