added files in git
This commit is contained in:
44
src/hooks/useChatSocket.js
Normal file
44
src/hooks/useChatSocket.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { Client } from "@stomp/stompjs";
|
||||
import SockJS from "sockjs-client";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
|
||||
export function useChatSocket(chatId, onMessage) {
|
||||
const clientRef = useRef(null);
|
||||
const [connected, setConnected] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chatId) return;
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
const socket = new SockJS(`${API_URL}/ws?token=${token}`);
|
||||
|
||||
const client = new Client({
|
||||
webSocketFactory: () => socket,
|
||||
reconnectDelay: 3000,
|
||||
onConnect: () => {
|
||||
setConnected(true);
|
||||
client.subscribe(`/topic/chat/${chatId}`, (msg) => {
|
||||
const body = JSON.parse(msg.body);
|
||||
onMessage(body);
|
||||
});
|
||||
},
|
||||
onDisconnect: () => setConnected(false),
|
||||
});
|
||||
|
||||
client.activate();
|
||||
clientRef.current = client;
|
||||
|
||||
return () => client.deactivate();
|
||||
}, [chatId, onMessage]);
|
||||
|
||||
const sendMessage = useCallback((content) => {
|
||||
if (!clientRef.current?.connected) return;
|
||||
clientRef.current.publish({
|
||||
destination: "/app/chat.send",
|
||||
body: JSON.stringify({ chatId, content }),
|
||||
});
|
||||
}, [chatId]);
|
||||
|
||||
return { connected, sendMessage };
|
||||
}
|
||||
62
src/hooks/useChats.js
Normal file
62
src/hooks/useChats.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
import { cachePostsFromChats, setPostForChat } from "../stores/chatPostStore";
|
||||
|
||||
function authHeaders() {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${localStorage.getItem("token")}`,
|
||||
};
|
||||
}
|
||||
|
||||
export function useChats() {
|
||||
const [chats, setChats] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchChats = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/v1/chats`, { headers: authHeaders() });
|
||||
const data = await res.json();
|
||||
// API may return an array directly or a wrapper object like { items: [] } / { content: [] }
|
||||
const list = Array.isArray(data) ? data : (data.items ?? data.content ?? []);
|
||||
cachePostsFromChats(list);
|
||||
setChats(list);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const createOrGetChat = useCallback(async (otherUserId, postId, postInfo) => {
|
||||
const url = new URL(`${API_URL}/api/v1/chats/with/${otherUserId}`);
|
||||
if (postId) url.searchParams.set("postId", postId);
|
||||
|
||||
const res = await fetch(url, { method: "POST", headers: authHeaders() });
|
||||
if (!res.ok) throw new Error("Не удалось создать чат");
|
||||
const chat = await res.json();
|
||||
if (postInfo && chat.id) {
|
||||
setPostForChat(chat.id, postInfo);
|
||||
}
|
||||
return chat;
|
||||
}, []);
|
||||
|
||||
const fetchMessages = useCallback(async (chatId) => {
|
||||
const res = await fetch(`${API_URL}/api/v1/chats/${chatId}/messages`, { headers: authHeaders() });
|
||||
const data = await res.json();
|
||||
// Normalize: array or wrapper object
|
||||
return Array.isArray(data) ? data : (data.items ?? data.content ?? []);
|
||||
}, []);
|
||||
|
||||
const markAsRead = useCallback(async (chatId) => {
|
||||
try {
|
||||
await fetch(`${API_URL}/api/v1/chats/${chatId}/read`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to mark as read:", e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { chats, loading, fetchChats, createOrGetChat, fetchMessages, markAsRead };
|
||||
}
|
||||
32
src/hooks/useDeleteChat.js
Normal file
32
src/hooks/useDeleteChat.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
|
||||
export function useDeleteChat() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const deleteChat = useCallback(async (chatId) => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const token = localStorage.getItem("token");
|
||||
const res = await fetch(`${API_URL}/api/v1/chats/${chatId}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Ошибка ${res.status}`);
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
setError(err.message || "Не удалось удалить чат");
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { deleteChat, loading, error };
|
||||
}
|
||||
40
src/hooks/useDeletePost.js
Normal file
40
src/hooks/useDeletePost.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
|
||||
/**
|
||||
* useDeletePost — хук для удаления объявления.
|
||||
*
|
||||
* @returns {Object} { deletePost, loading, error }
|
||||
*/
|
||||
export function useDeletePost() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const deletePost = useCallback(async (postId) => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem("token");
|
||||
const res = await fetch(`${API_URL}/api/v1/posts/${postId}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Ошибка ${res.status}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
setError(err.message || "Не удалось удалить объявление");
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { deletePost, loading, error };
|
||||
}
|
||||
74
src/hooks/useFavorites.js
Normal file
74
src/hooks/useFavorites.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import { useCallback } from 'react';
|
||||
import { API_URL } from '../config/authConstants';
|
||||
|
||||
/**
|
||||
* Хук для работы с лайками (избранное).
|
||||
* Использует backend API для добавления/удаления лайков.
|
||||
*/
|
||||
export function useFavorites() {
|
||||
/**
|
||||
* Переключает лайк на посте через API.
|
||||
* @param {number} postId - ID поста
|
||||
* @param {string} token - JWT токен авторизации
|
||||
* @returns {Promise<boolean>} - true если операция успешна
|
||||
*/
|
||||
const toggleFavorite = useCallback(async (postId, token) => {
|
||||
if (!token) {
|
||||
console.warn('Cannot toggle favorite: no token');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/v1/favorites/${postId}/toggle`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to toggle favorite: ${response.status}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error toggling favorite:', error);
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Загружает список лайкнутых постов текущего пользователя.
|
||||
* @param {string} token - JWT токен авторизации
|
||||
* @returns {Promise<Array>} - массив ID лайкнутых постов
|
||||
*/
|
||||
const loadFavorites = useCallback(async (token) => {
|
||||
if (!token) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/v1/favorites`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load favorites: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// API возвращает массив постов, извлекаем ID
|
||||
return Array.isArray(data) ? data.map(post => post.id) : [];
|
||||
} catch (error) {
|
||||
console.error('Error loading favorites:', error);
|
||||
return [];
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
toggleFavorite,
|
||||
loadFavorites,
|
||||
};
|
||||
}
|
||||
80
src/hooks/usePostStatus.js
Normal file
80
src/hooks/usePostStatus.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useCallback } from 'react';
|
||||
import { API_URL } from '../config/authConstants';
|
||||
|
||||
/**
|
||||
* Хук для изменения статуса поста (ACTIVE/CLOSED).
|
||||
* Используется для отметки "Вещь найдена".
|
||||
*/
|
||||
export function usePostStatus() {
|
||||
/**
|
||||
* Изменяет статус поста на CLOSED (вещь найдена).
|
||||
* @param {number} postId - ID поста
|
||||
* @param {string} token - JWT токен авторизации
|
||||
* @returns {Promise<boolean>} - true если операция успешна
|
||||
*/
|
||||
const markAsFound = useCallback(async (postId, token) => {
|
||||
if (!token) {
|
||||
console.warn('Cannot mark post as found: no token');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Используем PATCH endpoint для обновления статуса
|
||||
const response = await fetch(`${API_URL}/api/v1/posts/${postId}/status`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ status: 'CLOSED' }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to update status: ${response.status}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error marking post as found:', error);
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Возвращает статус поста в ACTIVE (если нужно reopen).
|
||||
* @param {number} postId - ID поста
|
||||
* @param {string} token - JWT токен авторизации
|
||||
* @returns {Promise<boolean>} - true если операция успешна
|
||||
*/
|
||||
const reopenPost = useCallback(async (postId, token) => {
|
||||
if (!token) {
|
||||
console.warn('Cannot reopen post: no token');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/v1/posts/${postId}/status`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ status: 'ACTIVE' }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to update status: ${response.status}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error reopening post:', error);
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
markAsFound,
|
||||
reopenPost,
|
||||
};
|
||||
}
|
||||
35
src/hooks/useUnreadSocket.js
Normal file
35
src/hooks/useUnreadSocket.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Client } from "@stomp/stompjs";
|
||||
import SockJS from "sockjs-client";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
import { useUnread } from "../context/UnreadContext";
|
||||
import { getActiveChatId } from "../stores/activeChatStore";
|
||||
|
||||
export function useUnreadSocket(userId) {
|
||||
const { setChatUnread } = useUnread();
|
||||
const clientRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userId) return;
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
const socket = new SockJS(`${API_URL}/ws?token=${token}`);
|
||||
|
||||
const client = new Client({
|
||||
webSocketFactory: () => socket,
|
||||
reconnectDelay: 3000,
|
||||
onConnect: () => {
|
||||
client.subscribe(`/topic/user/${userId}/unread`, (msg) => {
|
||||
const body = JSON.parse(msg.body);
|
||||
if (body.chatId === getActiveChatId()) return;
|
||||
setChatUnread(body.chatId, body.unreadCount);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
client.activate();
|
||||
clientRef.current = client;
|
||||
|
||||
return () => client.deactivate();
|
||||
}, [userId, setChatUnread]);
|
||||
}
|
||||
57
src/hooks/useUpdateProfile.js
Normal file
57
src/hooks/useUpdateProfile.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useState } from "react";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
|
||||
/**
|
||||
* useUpdateProfile — хук для обновления профиля пользователя.
|
||||
* Отправляет PUT /api/v1/users/{id} с FormData (поддерживает загрузку аватара).
|
||||
*/
|
||||
export function useUpdateProfile() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const updateProfile = async (userId, profileData) => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
// Используем FormData — чтобы отправить файл аватара вместе с текстовыми полями
|
||||
const formData = new FormData();
|
||||
formData.append("id", userId);
|
||||
formData.append("firstName", profileData.firstName || "");
|
||||
formData.append("lastName", profileData.lastName || "");
|
||||
formData.append("login", profileData.login || "");
|
||||
formData.append("phone", profileData.phone || "");
|
||||
formData.append("email", profileData.email || "");
|
||||
|
||||
// Если avatar — это File (новый файл), отправляем как часть FormData
|
||||
if (profileData.avatar instanceof File) {
|
||||
formData.append("avatar", profileData.avatar);
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_URL}/api/v1/users/${userId}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.message || `Ошибка ${res.status}`);
|
||||
}
|
||||
|
||||
const updatedUser = await res.json();
|
||||
return updatedUser;
|
||||
} catch (err) {
|
||||
setError(err.message || "Не удалось сохранить профиль");
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return { updateProfile, loading, error };
|
||||
}
|
||||
Reference in New Issue
Block a user