From 9f2f6f86f9f4b26bc73c955e9f9e8927a5fec85b Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Sat, 30 Aug 2025 23:01:59 +0300 Subject: [PATCH] Implement profile --- frontend/src/{chat => __chat}/chat.ts | 0 frontend/src/{chat => __chat}/contextMenu.ts | 0 frontend/src/{chat => __chat}/dm.ts | 0 frontend/src/{chat => __chat}/panel.ts | 12 +- .../src/{chat => __chat}/profileDialog.ts | 0 .../src/{navigation.ts => __navigation.__ts} | 6 +- .../{userPanel => __userPanel}/profile/api.ts | 0 .../profile/editor.ts | 0 .../profile/imageCropper.ts | 0 .../profile/profile.ts | 0 .../profile/types.ts | 0 .../profile/upload.ts | 0 .../{userPanel => __userPanel}/settings.ts | 0 .../{userPanel => __userPanel}/userpanel.ts | 2 +- frontend/src/auth/api.ts | 71 ++---- frontend/src/auth/crypto.ts | 18 +- frontend/src/core/init.ts | 6 - frontend/src/ui/api/profileApi.ts | 92 ++++++++ .../src/ui/components/chat/ChatHeader.tsx | 15 +- .../ui/components/profile/ImageCropper.tsx | 168 +++++++++++++++ .../ui/components/profile/ProfileDialog.tsx | 204 ++++++++++++++---- frontend/src/ui/hooks/useChat.ts | 2 +- frontend/src/ui/hooks/useProfile.ts | 98 +++++++++ frontend/src/ui/screen/ChatScreen.tsx | 3 +- frontend/src/ui/state.ts | 34 ++- frontend/tsconfig.json | 3 +- 26 files changed, 602 insertions(+), 132 deletions(-) rename frontend/src/{chat => __chat}/chat.ts (100%) rename frontend/src/{chat => __chat}/contextMenu.ts (100%) rename frontend/src/{chat => __chat}/dm.ts (100%) rename frontend/src/{chat => __chat}/panel.ts (94%) rename frontend/src/{chat => __chat}/profileDialog.ts (100%) rename frontend/src/{navigation.ts => __navigation.__ts} (88%) rename frontend/src/{userPanel => __userPanel}/profile/api.ts (100%) rename frontend/src/{userPanel => __userPanel}/profile/editor.ts (100%) rename frontend/src/{userPanel => __userPanel}/profile/imageCropper.ts (100%) rename frontend/src/{userPanel => __userPanel}/profile/profile.ts (100%) rename frontend/src/{userPanel => __userPanel}/profile/types.ts (100%) rename frontend/src/{userPanel => __userPanel}/profile/upload.ts (100%) rename frontend/src/{userPanel => __userPanel}/settings.ts (100%) rename frontend/src/{userPanel => __userPanel}/userpanel.ts (98%) create mode 100644 frontend/src/ui/api/profileApi.ts create mode 100644 frontend/src/ui/components/profile/ImageCropper.tsx create mode 100644 frontend/src/ui/hooks/useProfile.ts diff --git a/frontend/src/chat/chat.ts b/frontend/src/__chat/chat.ts similarity index 100% rename from frontend/src/chat/chat.ts rename to frontend/src/__chat/chat.ts diff --git a/frontend/src/chat/contextMenu.ts b/frontend/src/__chat/contextMenu.ts similarity index 100% rename from frontend/src/chat/contextMenu.ts rename to frontend/src/__chat/contextMenu.ts diff --git a/frontend/src/chat/dm.ts b/frontend/src/__chat/dm.ts similarity index 100% rename from frontend/src/chat/dm.ts rename to frontend/src/__chat/dm.ts diff --git a/frontend/src/chat/panel.ts b/frontend/src/__chat/panel.ts similarity index 94% rename from frontend/src/chat/panel.ts rename to frontend/src/__chat/panel.ts index 7f888c3..8614d5b 100644 --- a/frontend/src/chat/panel.ts +++ b/frontend/src/__chat/panel.ts @@ -1,10 +1,10 @@ -import { authToken, currentUser, getAuthHeaders } from "../auth/api"; -import { API_BASE_URL } from "../core/config"; +import { authToken, currentUser, getAuthHeaders } from "../auth/api.ts"; +import { API_BASE_URL } from "../core/config.ts"; import type { Message, Messages, WebSocketMessage } from "../core/types"; -import { request } from "../websocket"; -import { addMessage } from "./chat"; -import { show as showContextMenu } from "./contextMenu"; -import { show as showProfileDialog } from "./profileDialog"; +import { request } from "../websocket.ts"; +import { addMessage } from "./chat.ts"; +import { show as showContextMenu } from "./contextMenu.ts"; +import { show as showProfileDialog } from "./profileDialog.ts"; const titleEl = document.getElementById("chat-name")!; const messages = document.getElementById("chat-messages")!; diff --git a/frontend/src/chat/profileDialog.ts b/frontend/src/__chat/profileDialog.ts similarity index 100% rename from frontend/src/chat/profileDialog.ts rename to frontend/src/__chat/profileDialog.ts diff --git a/frontend/src/navigation.ts b/frontend/src/__navigation.__ts similarity index 88% rename from frontend/src/navigation.ts rename to frontend/src/__navigation.__ts index 81081dc..ca1e994 100644 --- a/frontend/src/navigation.ts +++ b/frontend/src/__navigation.__ts @@ -1,6 +1,6 @@ -import { clearAlerts } from "./auth/auth"; -import { publicChatPanel } from "./chat/chat"; -import { id } from "./utils/utils"; +import { clearAlerts } from "./auth/auth.ts"; +import { publicChatPanel } from "./chat/chat.ts"; +import { id } from "./utils/utils.ts"; const loginForm = id("login-form"); const registerForm = id("register-form"); diff --git a/frontend/src/userPanel/profile/api.ts b/frontend/src/__userPanel/profile/api.ts similarity index 100% rename from frontend/src/userPanel/profile/api.ts rename to frontend/src/__userPanel/profile/api.ts diff --git a/frontend/src/userPanel/profile/editor.ts b/frontend/src/__userPanel/profile/editor.ts similarity index 100% rename from frontend/src/userPanel/profile/editor.ts rename to frontend/src/__userPanel/profile/editor.ts diff --git a/frontend/src/userPanel/profile/imageCropper.ts b/frontend/src/__userPanel/profile/imageCropper.ts similarity index 100% rename from frontend/src/userPanel/profile/imageCropper.ts rename to frontend/src/__userPanel/profile/imageCropper.ts diff --git a/frontend/src/userPanel/profile/profile.ts b/frontend/src/__userPanel/profile/profile.ts similarity index 100% rename from frontend/src/userPanel/profile/profile.ts rename to frontend/src/__userPanel/profile/profile.ts diff --git a/frontend/src/userPanel/profile/types.ts b/frontend/src/__userPanel/profile/types.ts similarity index 100% rename from frontend/src/userPanel/profile/types.ts rename to frontend/src/__userPanel/profile/types.ts diff --git a/frontend/src/userPanel/profile/upload.ts b/frontend/src/__userPanel/profile/upload.ts similarity index 100% rename from frontend/src/userPanel/profile/upload.ts rename to frontend/src/__userPanel/profile/upload.ts diff --git a/frontend/src/userPanel/settings.ts b/frontend/src/__userPanel/settings.ts similarity index 100% rename from frontend/src/userPanel/settings.ts rename to frontend/src/__userPanel/settings.ts diff --git a/frontend/src/userPanel/userpanel.ts b/frontend/src/__userPanel/userpanel.ts similarity index 98% rename from frontend/src/userPanel/userpanel.ts rename to frontend/src/__userPanel/userpanel.ts index 1ec52bd..537a2ec 100644 --- a/frontend/src/userPanel/userpanel.ts +++ b/frontend/src/__userPanel/userpanel.ts @@ -8,7 +8,7 @@ import { Dialog } from "mdui/components/dialog"; import { loadProfilePicture } from "./profile/upload"; import { id } from "../utils/utils"; -import { publicChatPanel } from "../chat/chat"; +import { publicChatPanel } from "../chat/chat.ts"; // сварачивание и разворачивание чата const chatCollapseBtn = id('hide-chat')!; diff --git a/frontend/src/auth/api.ts b/frontend/src/auth/api.ts index 1a4812e..57b4cd4 100644 --- a/frontend/src/auth/api.ts +++ b/frontend/src/auth/api.ts @@ -4,57 +4,20 @@ import type { Headers, User, WebSocketMessage } from "../core/types"; import { clearAlerts } from "./auth"; import { request } from "../websocket"; -/** - * Current authenticated user information - * @type {User | null} - */ -export let currentUser: User | null = null; - -/** - * JWT authentication token - * @type {string | null} - */ -export let authToken: string | null = null; - -/** - * Sets the current user to the values provided. - * @param token The authentication JWT token. - * @param user The current authenticated user. - */ -export function setUser(token: string, user: User) { - authToken = token - currentUser = user - - try { - const payload: WebSocketMessage = { - type: "ping", - credentials: { - scheme: "Bearer", - credentials: authToken - }, - data: {} - } - - request(payload).then(() => { - console.log("Ping succeeded") - }) - } catch {} -} - /** * Generates authentication headers for API requests * @param {boolean} json - Whether to include JSON content type header * @returns {Headers} Headers object with authentication and content type */ -export function getAuthHeaders(json: boolean = true): Headers { +export function getAuthHeaders(token: string | null, json: boolean = true): Headers { const headers: Headers = {}; if (json) { headers["Content-Type"] = "application/json"; } - if (authToken) { - headers['Authorization'] = `Bearer ${authToken}`; + if (token) { + headers['Authorization'] = `Bearer ${token}`; } return headers; } @@ -71,18 +34,18 @@ export async function checkAuthStatus(): Promise { /** * Logs out the current user and clears session data */ -export async function logout(): Promise { - try { - await fetch(`${API_BASE_URL}/logout`, { - method: 'GET', - headers: getAuthHeaders() - }); - } catch (error) { - console.error('Logout error:', error); - } +// export async function logout(): Promise { +// try { +// await fetch(`${API_BASE_URL}/logout`, { +// method: 'GET', +// headers: getAuthHeaders() +// }); +// } catch (error) { +// console.error('Logout error:', error); +// } - currentUser = null; - authToken = null; - // showLogin(); - clearAlerts(); -} \ No newline at end of file +// currentUser = null; +// authToken = null; +// // showLogin(); +// clearAlerts(); +// } \ No newline at end of file diff --git a/frontend/src/auth/crypto.ts b/frontend/src/auth/crypto.ts index d416f7a..7b18e6c 100644 --- a/frontend/src/auth/crypto.ts +++ b/frontend/src/auth/crypto.ts @@ -8,8 +8,8 @@ import type { BackupBlob, UploadPublicKeyRequest } from "../core/types"; let currentPublicKey: Uint8Array | null = null; let currentPrivateKey: Uint8Array | null = null; -async function fetchPublicKey(token?: string): Promise { - const headers = token ? { 'Authorization': `Bearer ${token}` } : getAuthHeaders(true); +async function fetchPublicKey(token: string): Promise { + const headers = getAuthHeaders(token, true); const res = await fetch(`${API_BASE_URL}/crypto/public-key`, { method: "GET", headers }); if (!res.ok) return null; const data = await res.json(); @@ -17,12 +17,12 @@ async function fetchPublicKey(token?: string): Promise { return ub64(data.publicKey); } -async function uploadPublicKey(publicKey: Uint8Array, token?: string): Promise { +async function uploadPublicKey(publicKey: Uint8Array, token: string): Promise { const payload: UploadPublicKeyRequest = { publicKey: b64(publicKey) } - const headers = token ? { 'Authorization': `Bearer ${token}` } : getAuthHeaders(true); + const headers = getAuthHeaders(token, true); await fetch(`${API_BASE_URL}/crypto/public-key`, { method: "POST", headers, @@ -30,8 +30,8 @@ async function uploadPublicKey(publicKey: Uint8Array, token?: string): Promise { - const headers = token ? { 'Authorization': `Bearer ${token}` } : getAuthHeaders(true); +async function fetchBackupBlob(token: string): Promise { + const headers = getAuthHeaders(token, true); const res = await fetch(`${API_BASE_URL}/crypto/backup`, { method: "GET", headers @@ -44,10 +44,10 @@ async function fetchBackupBlob(token?: string): Promise { } } -async function uploadBackupBlob(blobJson: string, token?: string): Promise { +async function uploadBackupBlob(blobJson: string, token: string): Promise { const payload: BackupBlob = { blob: blobJson } - const headers = token ? { 'Authorization': `Bearer ${token}` } : getAuthHeaders(true); + const headers = getAuthHeaders(token, true); await fetch(`${API_BASE_URL}/crypto/backup`, { method: "POST", headers, @@ -65,7 +65,7 @@ export function getCurrentKeys(): UserKeyPairMemory | null { return null; } -export async function ensureKeysOnLogin(password: string, token?: string): Promise { +export async function ensureKeysOnLogin(password: string, token: string): Promise { // Try to restore from backup const blobJson = await fetchBackupBlob(token); if (blobJson) { diff --git a/frontend/src/core/init.ts b/frontend/src/core/init.ts index dde6cf7..e18753e 100644 --- a/frontend/src/core/init.ts +++ b/frontend/src/core/init.ts @@ -5,12 +5,6 @@ * @version 1.0.0 */ -import { showLogin } from "../navigation"; import { PRODUCT_NAME } from "./config"; -showLogin(); - -document.querySelectorAll(".product-name").forEach(el => { - el.textContent = PRODUCT_NAME; -}); document.title = PRODUCT_NAME; \ No newline at end of file diff --git a/frontend/src/ui/api/profileApi.ts b/frontend/src/ui/api/profileApi.ts new file mode 100644 index 0000000..a60f820 --- /dev/null +++ b/frontend/src/ui/api/profileApi.ts @@ -0,0 +1,92 @@ +import { getAuthHeaders } from "../../auth/api"; +import { API_BASE_URL } from "../../core/config"; + +export interface ProfileData { + profile_picture?: string; + nickname?: string; + description?: string; +} + +export interface UploadResponse { + profile_picture_url: string; +} + +/** + * Loads user profile data from the server + */ +export async function loadProfile(token: string): Promise { + try { + const response = await fetch(`${API_BASE_URL}/user/profile`, { + headers: getAuthHeaders(token) + }); + + if (response.ok) { + return await response.json(); + } + + return null; + } catch (error) { + console.error('Error loading profile:', error); + return null; + } +} + +/** + * Uploads a profile picture to the server + */ +export async function uploadProfilePicture(token: string, file: Blob): Promise { + try { + const formData = new FormData(); + formData.append('profile_picture', file, 'profile_picture.jpg'); + + const response = await fetch(`${API_BASE_URL}/upload-profile-picture`, { + method: 'POST', + body: formData, + headers: getAuthHeaders(token, false) + }); + + if (response.ok) { + return await response.json(); + } + return null; + } catch (error) { + console.error('Upload error:', error); + return null; + } +} + +/** + * Updates user profile information + */ +export async function updateProfile(token: string, data: Partial): Promise { + try { + const response = await fetch(`${API_BASE_URL}/user/profile`, { + method: 'PUT', + headers: getAuthHeaders(token), + body: JSON.stringify(data) + }); + + return response.ok; + } catch (error) { + console.error('Error updating profile:', error); + return false; + } +} + +/** + * Updates user bio + */ +export async function updateBio(token: string, bio: string): Promise { + try { + const response = await fetch(`${API_BASE_URL}/user/bio`, { + method: 'PUT', + headers: getAuthHeaders(token), + body: JSON.stringify({ bio }) + }); + + return response.ok; + } catch (error) { + console.error('Error updating bio:', error); + return false; + } +} diff --git a/frontend/src/ui/components/chat/ChatHeader.tsx b/frontend/src/ui/components/chat/ChatHeader.tsx index 25786a0..7c9fdfd 100644 --- a/frontend/src/ui/components/chat/ChatHeader.tsx +++ b/frontend/src/ui/components/chat/ChatHeader.tsx @@ -1,19 +1,32 @@ import { PRODUCT_NAME } from "../../../core/config"; import { useDialog } from "../../contexts/DialogContext"; +import { useProfile } from "../../hooks/useProfile"; +import defaultAvatar from "../../../resources/images/default-avatar.png"; export function ChatHeader() { const { openProfile } = useDialog(); + const { profileData } = useProfile(); const handleProfileClick = () => { openProfile(); }; + const profilePictureUrl = profileData?.profile_picture || defaultAvatar; + return (
{PRODUCT_NAME}
diff --git a/frontend/src/ui/components/profile/ImageCropper.tsx b/frontend/src/ui/components/profile/ImageCropper.tsx new file mode 100644 index 0000000..2ca162e --- /dev/null +++ b/frontend/src/ui/components/profile/ImageCropper.tsx @@ -0,0 +1,168 @@ +import { useEffect, useRef, useState } from "react"; + +interface ImageCropperProps { + onCrop: (croppedImageData: string) => void; + onCancel: () => void; + imageFile: File | null; +} + +export function ImageCropper({ onCrop, onCancel, imageFile }: ImageCropperProps) { + const canvasRef = useRef(null); + const imageRef = useRef(null); + const [isLoaded, setIsLoaded] = useState(false); + const [cropArea, setCropArea] = useState({ x: 0, y: 0, width: 200, height: 200 }); + const [isDragging, setIsDragging] = useState(false); + const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); + + useEffect(() => { + if (!imageFile) return; + + const reader = new FileReader(); + reader.onload = (e) => { + if (imageRef.current) { + imageRef.current.src = e.target?.result as string; + imageRef.current.onload = () => { + setIsLoaded(true); + // Initialize crop area to center of image + if (imageRef.current) { + const img = imageRef.current; + const size = Math.min(img.naturalWidth, img.naturalHeight) * 0.8; + setCropArea({ + x: (img.naturalWidth - size) / 2, + y: (img.naturalHeight - size) / 2, + width: size, + height: size + }); + } + }; + } + }; + reader.readAsDataURL(imageFile); + }, [imageFile]); + + const handleMouseDown = (e: React.MouseEvent) => { + if (!isLoaded) return; + + const rect = canvasRef.current?.getBoundingClientRect(); + if (!rect) return; + + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + // Check if click is within crop area + if (x >= cropArea.x && x <= cropArea.x + cropArea.width && + y >= cropArea.y && y <= cropArea.y + cropArea.height) { + setIsDragging(true); + setDragStart({ x: x - cropArea.x, y: y - cropArea.y }); + } + }; + + const handleMouseMove = (e: React.MouseEvent) => { + if (!isDragging || !isLoaded) return; + + const rect = canvasRef.current?.getBoundingClientRect(); + if (!rect || !imageRef.current) return; + + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + const newX = Math.max(0, Math.min(x - dragStart.x, imageRef.current.naturalWidth - cropArea.width)); + const newY = Math.max(0, Math.min(y - dragStart.y, imageRef.current.naturalHeight - cropArea.height)); + + setCropArea(prev => ({ ...prev, x: newX, y: newY })); + }; + + const handleMouseUp = () => { + setIsDragging(false); + }; + + const handleCrop = () => { + if (!canvasRef.current || !imageRef.current || !isLoaded) return; + + const canvas = canvasRef.current; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + // Set canvas size to crop area + canvas.width = cropArea.width; + canvas.height = cropArea.height; + + // Draw cropped portion + ctx.drawImage( + imageRef.current, + cropArea.x, cropArea.y, cropArea.width, cropArea.height, + 0, 0, cropArea.width, cropArea.height + ); + + // Convert to data URL + const croppedImageData = canvas.toDataURL('image/jpeg', 0.9); + onCrop(croppedImageData); + }; + + const drawCropArea = () => { + if (!canvasRef.current || !imageRef.current || !isLoaded) return; + + const canvas = canvasRef.current; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + // Clear canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw image + ctx.drawImage(imageRef.current, 0, 0, canvas.width, canvas.height); + + // Draw crop overlay + ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Clear crop area + ctx.globalCompositeOperation = 'destination-out'; + ctx.fillRect(cropArea.x, cropArea.y, cropArea.width, cropArea.height); + + // Draw crop border + ctx.globalCompositeOperation = 'source-over'; + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 2; + ctx.strokeRect(cropArea.x, cropArea.y, cropArea.width, cropArea.height); + }; + + useEffect(() => { + drawCropArea(); + }, [cropArea, isLoaded]); + + if (!imageFile) return null; + + return ( +
+ + Crop source +
+ + Обрезать + + + Отмена + +
+
+ ); +} diff --git a/frontend/src/ui/components/profile/ProfileDialog.tsx b/frontend/src/ui/components/profile/ProfileDialog.tsx index 53e8f86..a012957 100644 --- a/frontend/src/ui/components/profile/ProfileDialog.tsx +++ b/frontend/src/ui/components/profile/ProfileDialog.tsx @@ -1,55 +1,179 @@ -import { useState, type FormEvent } from "react"; +import { useState, useEffect, useRef, type FormEvent } from "react"; import defaultAvatar from "../../../resources/images/default-avatar.png"; import type { TextField } from "mdui/components/text-field"; import type { DialogProps } from "../../../core/types"; import { MaterialDialog } from "../Dialog"; +import { useProfile } from "../../hooks/useProfile"; +import { ImageCropper } from "./ImageCropper"; export function ProfileDialog({ isOpen, onOpenChange }: DialogProps) { - const [username, setUsername] = useState("user123"); + const { profileData, isLoading, isUpdating, updateProfileData, uploadProfilePictureData } = useProfile(); + const [username, setUsername] = useState(""); const [description, setDescription] = useState(""); + const [selectedImage, setSelectedImage] = useState(null); + const [showCropper, setShowCropper] = useState(false); + + const fileInputRef = useRef(null); - const handleSubmit = (e: React.FormEvent) => { + // Update form fields when profile data changes + useEffect(() => { + if (profileData) { + setUsername(profileData.nickname || ""); + setDescription(profileData.description || ""); + } + }, [profileData]); + + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - // TODO: Implement profile update logic - console.log("Profile update:", { username, description }); - onOpenChange(false); + + const success = await updateProfileData({ + nickname: username.trim() || undefined, + description: description.trim() || undefined + }); + + if (success) { + onOpenChange(false); + } }; - return ( - -
-
-
- Ваше фото - - -
- ) => setUsername((e.target as TextField).value)} - autocomplete="username"> - -
+ const handleImageSelect = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file && file.type.startsWith('image/')) { + setSelectedImage(file); + setShowCropper(true); + } + }; -
- ) => setDescription((e.target as TextField).value)} - placeholder="Расскажите о себе..." - autocomplete="none"> - -
- Сохранить изменения - onOpenChange(false)}>Закрыть + const handleCropComplete = async (croppedImageData: string) => { + try { + // Convert data URL to blob + const response = await fetch(croppedImageData); + const blob = await response.blob(); + + const success = await uploadProfilePictureData(blob); + if (success) { + setShowCropper(false); + setSelectedImage(null); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + } + } catch (error) { + console.error('Error processing cropped image:', error); + } + }; + + const handleCropCancel = () => { + setShowCropper(false); + setSelectedImage(null); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + const handleUploadClick = () => { + fileInputRef.current?.click(); + }; + + const profilePictureUrl = profileData?.profile_picture || defaultAvatar; + + return ( + <> + +
+
+
+ Ваше фото { + const target = e.target as HTMLImageElement; + target.src = defaultAvatar; + }} + /> + + +
+ ) => setUsername((e.target as TextField).value)} + autocomplete="username" + disabled={isLoading || isUpdating} + />
- -
-
+ +
+ ) => setDescription((e.target as TextField).value)} + placeholder="Расскажите о себе..." + autocomplete="none" + disabled={isLoading || isUpdating} + /> +
+ + {isUpdating ? "Сохранение..." : "Сохранить изменения"} + + onOpenChange(false)} + disabled={isUpdating} + > + Закрыть + +
+ +
+ + + {/* Image Cropper Dialog */} + +
+
+

Обрезать фото профиля

+ +
+
+ +
+
+
+ ); } diff --git a/frontend/src/ui/hooks/useChat.ts b/frontend/src/ui/hooks/useChat.ts index 282a947..9653c46 100644 --- a/frontend/src/ui/hooks/useChat.ts +++ b/frontend/src/ui/hooks/useChat.ts @@ -27,7 +27,7 @@ export function useChat() { try { const response = await fetch(`${API_BASE_URL}/get_messages`, { - headers: getAuthHeaders() + headers: getAuthHeaders(user.authToken) }); if (response.ok) { diff --git a/frontend/src/ui/hooks/useProfile.ts b/frontend/src/ui/hooks/useProfile.ts new file mode 100644 index 0000000..4922d9d --- /dev/null +++ b/frontend/src/ui/hooks/useProfile.ts @@ -0,0 +1,98 @@ +import { useState, useCallback, useEffect } from "react"; +import { useAppState } from "../state"; +import { loadProfile, updateProfile, uploadProfilePicture, type ProfileData } from "../api/profileApi"; +import { showSuccess, showError } from "../../utils/notification"; + +export function useProfile() { + const { user } = useAppState(); + const [profileData, setProfileData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isUpdating, setIsUpdating] = useState(false); + + // Load profile data + const loadProfileData = useCallback(async () => { + if (!user.authToken) return; + + setIsLoading(true); + try { + const data = await loadProfile(user.authToken); + if (data) { + setProfileData(data); + } + } catch (error) { + console.error('Error loading profile:', error); + showError('Ошибка при загрузке профиля'); + } finally { + setIsLoading(false); + } + }, [user.authToken]); + + // Update profile + const updateProfileData = useCallback(async (data: Partial) => { + if (!user.authToken) return false; + + setIsUpdating(true); + try { + const success = await updateProfile(user.authToken, data); + if (success) { + // Reload profile data to get updated information + await loadProfileData(); + showSuccess('Профиль обновлен!'); + return true; + } else { + showError('Ошибка при обновлении профиля'); + return false; + } + } catch (error) { + console.error('Error updating profile:', error); + showError('Ошибка при обновлении профиля'); + return false; + } finally { + setIsUpdating(false); + } + }, [user.authToken, loadProfileData]); + + // Upload profile picture + const uploadProfilePictureData = useCallback(async (file: Blob) => { + if (!user.authToken) return false; + + setIsUpdating(true); + try { + const result = await uploadProfilePicture(user.authToken, file); + if (result) { + // Update profile data with new picture URL + setProfileData(prev => prev ? { + ...prev, + profile_picture: result.profile_picture_url + } : null); + showSuccess('Фото профиля обновлено!'); + return true; + } else { + showError('Ошибка при загрузке фото'); + return false; + } + } catch (error) { + console.error('Error uploading profile picture:', error); + showError('Ошибка при загрузке фото'); + return false; + } finally { + setIsUpdating(false); + } + }, [user.authToken]); + + // Load profile data when user is authenticated + useEffect(() => { + if (user.authToken) { + loadProfileData(); + } + }, [user.authToken, loadProfileData]); + + return { + profileData, + isLoading, + isUpdating, + loadProfileData, + updateProfileData, + uploadProfilePictureData + }; +} diff --git a/frontend/src/ui/screen/ChatScreen.tsx b/frontend/src/ui/screen/ChatScreen.tsx index c25baa7..416418d 100644 --- a/frontend/src/ui/screen/ChatScreen.tsx +++ b/frontend/src/ui/screen/ChatScreen.tsx @@ -1,5 +1,4 @@ import { ChatInterface } from "../components/chat/ChatInterface"; -import { ProfileDialog } from "../components/profile/ProfileDialog"; import { CropperDialog } from "../components/profile/CropperDialog"; import { SettingsDialog } from "../components/settings/SettingsDialog"; import { MessageContextMenu } from "../components/chat/MessageContextMenu"; @@ -11,12 +10,12 @@ export default function ChatScreen() { return ( <> - + {}} /> ); } \ No newline at end of file diff --git a/frontend/src/ui/state.ts b/frontend/src/ui/state.ts index 7d414fb..60543d2 100644 --- a/frontend/src/ui/state.ts +++ b/frontend/src/ui/state.ts @@ -1,5 +1,6 @@ import { create } from "zustand"; -import type { Message, User, UserProfile } from "../core/types"; +import type { Message, User, UserProfile, WebSocketMessage } from "../core/types"; +import { request } from "../websocket"; type Page = "login" | "register" | "chat" @@ -113,13 +114,30 @@ export const useAppState = create((set, get) => ({ currentUser: null, authToken: null }, - setUser: (token: string, user: User) => set((state) => ({ - user: { - currentUser: user, - authToken: token - } - })), - logout: () => set((state) => ({ + setUser: (token: string, user: User) => { + set(() => ({ + user: { + currentUser: user, + authToken: token + } + })); + + try { + const payload: WebSocketMessage = { + type: "ping", + credentials: { + scheme: "Bearer", + credentials: token + }, + data: {} + } + + request(payload).then(() => { + console.log("Ping succeeded") + }) + } catch {} + }, + logout: () => set(() => ({ user: { currentUser: null, authToken: null diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 2dcd702..96482d5 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -23,5 +23,6 @@ "jsx": "react-jsx", "jsxImportSource": "react" }, - "include": ["src", "electron.d.ts"] + "include": ["src", "electron.d.ts"], + "exclude": ["**/__*/**", "__*"] } \ No newline at end of file