mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Fix user profile dialog
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -187,6 +187,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
|
||||||
|
mdui-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +39,7 @@ export function ChatMessages() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="chat-messages" id="chat-messages">
|
<div className="chat-messages" id="chat-messages">
|
||||||
{messages.map((message) => (
|
{messages.map((message) => (
|
||||||
<Message
|
<Message
|
||||||
@@ -27,8 +48,22 @@ export function ChatMessages() {
|
|||||||
isAuthor={message.username === user.currentUser?.username}
|
isAuthor={message.username === user.currentUser?.username}
|
||||||
onProfileClick={handleProfileClick}
|
onProfileClick={handleProfileClick}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
|
isLoadingProfile={isLoadingProfile}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<UserProfileDialog
|
||||||
|
isOpen={profileDialogOpen}
|
||||||
|
onOpenChange={async (value) => {
|
||||||
|
setProfileDialogOpen(value);
|
||||||
|
if (!value) {
|
||||||
|
await delay(1000);
|
||||||
|
setSelectedUserProfile(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
userProfile={selectedUserProfile}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,30 +1,56 @@
|
|||||||
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>
|
}
|
||||||
|
|
||||||
|
export function UserProfileDialog({ isOpen, onOpenChange, userProfile }: UserProfileDialogProps) {
|
||||||
|
const content = userProfile ? (
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<div className="profile-picture-section">
|
<div className="profile-picture-section">
|
||||||
<img className="profile-picture" alt="Profile Picture" />
|
<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>
|
||||||
<div className="profile-info">
|
<div className="profile-info">
|
||||||
<div className="username-section">
|
<div className="username-section">
|
||||||
<h4 className="username"></h4>
|
<h4 className="username">{userProfile.username}</h4>
|
||||||
<div className="online-status"></div>
|
<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="bio-section">
|
<div className="bio-section">
|
||||||
<label>Bio:</label>
|
<label>Bio:</label>
|
||||||
<div className="bio-display"></div>
|
<div className="bio-display">
|
||||||
|
{userProfile.bio || "No bio available."}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="profile-stats">
|
<div className="profile-stats">
|
||||||
<div className="stat">
|
<div className="stat">
|
||||||
<span className="stat-label">Member since:</span>
|
<span className="stat-label">Member since:</span>
|
||||||
<span className="stat-value member-since"></span>
|
<span className="stat-value member-since">{formatTime(userProfile.created_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat">
|
<div className="stat">
|
||||||
<span className="stat-label">Last seen:</span>
|
<span className="stat-label">Last seen:</span>
|
||||||
<span className="stat-value last-seen"></span>
|
<span className="stat-value last-seen">{formatTime(userProfile.last_seen)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="profile-actions">
|
<div className="profile-actions">
|
||||||
@@ -35,6 +61,11 @@ export function UserProfileDialog({ isOpen, onOpenChange }: DialogProps) {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user