added files in git

This commit is contained in:
SlimusMinus
2026-07-03 00:04:56 +03:00
parent a5d457b07a
commit b924bc5f94
33 changed files with 2916 additions and 2 deletions

View 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
View 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 };
}

View 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 };
}

View 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
View 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,
};
}

View 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,
};
}

View 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]);
}

View 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 };
}