Update frontend for new backend

This commit is contained in:
IgorVolochay
2026-09-01 14:31:52 +03:00
parent bdf3d5aec1
commit 008dd1c29e
22 changed files with 1113 additions and 264 deletions
+247 -67
View File
@@ -1,6 +1,6 @@
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react';
import { api } from '../services/api';
import { currentUser, initTelegramApp } from '../services/auth';
import { currentUser, initTelegramApp, hapticImpact, hapticNotification } from '../services/auth';
const AppContext = createContext(null);
@@ -9,11 +9,13 @@ export function AppProvider({ children }) {
const [isLoading, setIsLoading] = useState(true);
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
const [isBanned, setIsBanned] = useState(false);
// Card queue
const [cardQueue, setCardQueue] = useState([]);
const [currentCardIndex, setCurrentCardIndex] = useState(0);
const [chosenCard, setChosenCard] = useState(null); // null | "A" | "B"
const [isLoadingCards, setIsLoadingCards] = useState(false);
// Panels
const [isMenuOpen, setIsMenuOpen] = useState(false);
@@ -22,84 +24,152 @@ export function AppProvider({ children }) {
// Toast
const [toast, setToast] = useState(null);
const toastTimeoutRef = useRef(null);
// User Profile Cache for comments
const userProfileCacheRef = useRef(new Map());
// Initialization ref for React StrictMode
const isInitializingRef = useRef(false);
// Current card helper
const currentCard = cardQueue[currentCardIndex] || null;
// Initialize app
useEffect(() => {
async function init() {
try {
initTelegramApp();
// Check/register user
const checkResult = await api.checkUser(currentUser.id);
if (!checkResult.result) {
await api.addUser({
user_id: currentUser.id,
username: currentUser.username,
first_name: currentUser.first_name,
last_name: currentUser.last_name,
photo_url: currentUser.photo_url,
});
}
const userResult = await api.getUser(currentUser.id);
if (!userResult.error) {
setUser(userResult.result);
}
// Load first batch of cards
await loadCards();
} catch (err) {
setError('Не удалось загрузить приложение');
console.error('Init error:', err);
} finally {
setIsLoading(false);
}
// Toast helper
const showToast = useCallback((message, duration = 2500) => {
if (toastTimeoutRef.current) {
clearTimeout(toastTimeoutRef.current);
}
init();
// eslint-disable-next-line react-hooks/exhaustive-deps
setToast(message);
toastTimeoutRef.current = setTimeout(() => setToast(null), duration);
}, []);
// Response interceptor helper
const handleApiResponse = useCallback((res) => {
if (res?.isBanned) {
setIsBanned(true);
}
if (res?.status === 429) {
showToast(typeof res.result === 'string' ? res.result : 'Слишком много запросов. Подождите немного.');
}
return res;
}, [showToast]);
// Load cards batch
const loadCards = useCallback(async () => {
const loadCards = useCallback(async (isManualRefresh = false) => {
setIsLoadingCards(true);
if (isManualRefresh) {
hapticImpact('light');
}
try {
const result = await api.getRandomCards(currentUser.id);
handleApiResponse(result);
if (!result.error && Array.isArray(result.result) && result.result.length > 0) {
setCardQueue(result.result);
setCurrentCardIndex(0);
setChosenCard(null);
if (isManualRefresh) {
showToast('Карточки обновлены!');
hapticNotification('success');
}
} else {
// No more cards or error
// Pool is empty or all cards seen
setCardQueue([]);
setCurrentCardIndex(0);
setChosenCard(null);
if (isManualRefresh) {
showToast('Новых карточек пока нет');
}
}
} catch (err) {
console.error('Load cards error:', err);
setError('Ошибка загрузки карточек');
}
}, []);
// Choose a card (A or B)
const chooseCard = useCallback((choice) => {
if (chosenCard) {
// Second tap on chosen card — go next
if (choice === chosenCard) {
goToNextCard();
if (isManualRefresh) {
showToast('Ошибка загрузки карточек');
}
return;
} finally {
setIsLoadingCards(false);
}
setChosenCard(choice);
// Fire select_choice to backend
if (currentCard) {
api.selectChoice(currentUser.id, currentCard.card_id, choice).catch(console.error);
}, [handleApiResponse, showToast]);
// Initialize app
const initApp = useCallback(async () => {
try {
initTelegramApp();
// Check/register user
const checkResult = await api.checkUser(currentUser.id);
handleApiResponse(checkResult);
if (checkResult?.isBanned) return;
if (!checkResult.error && !checkResult.result) {
const addResult = await api.addUser({
user_id: currentUser.id,
username: currentUser.username,
first_name: currentUser.first_name,
last_name: currentUser.last_name,
photo_url: currentUser.photo_url,
});
handleApiResponse(addResult);
if (addResult?.isBanned) return;
}
const userResult = await api.getUser(currentUser.id);
handleApiResponse(userResult);
if (userResult?.isBanned) return;
if (!userResult.error && userResult.result) {
setUser(userResult.result);
}
// Load first batch of cards
await loadCards();
} catch (err) {
setError('Не удалось загрузить приложение');
console.error('Init error:', err);
} finally {
setIsLoading(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [chosenCard, currentCard]);
}, [handleApiResponse, loadCards]);
useEffect(() => {
if (isInitializingRef.current) return;
isInitializingRef.current = true;
initApp();
}, [initApp]);
// User Profile resolver for comments
const getUserProfile = useCallback(async (userId) => {
if (!userId) return null;
if (userId === currentUser.id) {
return {
user_id: currentUser.id,
username: currentUser.username,
first_name: currentUser.first_name,
last_name: currentUser.last_name,
photo_url: currentUser.photo_url,
};
}
if (userProfileCacheRef.current.has(userId)) {
return userProfileCacheRef.current.get(userId);
}
try {
const res = await api.getUser(userId);
handleApiResponse(res);
if (!res.error && res.result) {
userProfileCacheRef.current.set(userId, res.result);
return res.result;
}
} catch (err) {
console.error('Failed to get user profile:', userId, err);
}
return null;
}, [handleApiResponse]);
// Go to next card
const goToNextCard = useCallback(async () => {
hapticImpact('light');
const nextIndex = currentCardIndex + 1;
if (nextIndex < cardQueue.length) {
setCurrentCardIndex(nextIndex);
@@ -110,50 +180,155 @@ export function AppProvider({ children }) {
}
}, [currentCardIndex, cardQueue.length, loadCards]);
// Choose a card (A or B)
const chooseCard = useCallback((choice) => {
if (chosenCard) {
// Second tap on chosen card — go next
if (choice === chosenCard) {
goToNextCard();
}
return;
}
hapticImpact('medium');
setChosenCard(choice);
if (currentCard) {
api.selectChoice(currentUser.id, currentCard.card_id, choice)
.then(handleApiResponse)
.catch(console.error);
}
}, [chosenCard, currentCard, goToNextCard, handleApiResponse]);
const syncCardComments = useCallback((cardId, commentsArrayOrIds) => {
if (!Array.isArray(commentsArrayOrIds)) return;
setCardQueue((prev) => {
const targetCard = prev.find((c) => c.card_id === cardId);
if (!targetCard) return prev;
if (targetCard.comments && targetCard.comments.length === commentsArrayOrIds.length) {
return prev; // No change, avoid re-render
}
return prev.map((c) =>
c.card_id === cardId
? { ...c, comments: commentsArrayOrIds }
: c
);
});
}, []);
const addCommentToCard = useCallback((cardId, commentId) => {
setCardQueue((prev) =>
prev.map((c) =>
c.card_id === cardId
? {
...c,
comments: c.comments ? [...c.comments, commentId] : [commentId],
}
: c
)
);
}, []);
// Auto-sync real comments count for current card from /get_comments
useEffect(() => {
const cardId = currentCard?.card_id;
if (!cardId) return;
let isMounted = true;
api.getComments(cardId)
.then((res) => {
if (isMounted && !res.error && Array.isArray(res.result)) {
syncCardComments(cardId, res.result.map((c) => c.comment_id));
}
})
.catch(() => { });
return () => {
isMounted = false;
};
}, [currentCard?.card_id, syncCardComments]);
// Reactions
const likeCard = useCallback(async () => {
if (!currentCard || !chosenCard) return;
hapticImpact('light');
const result = await api.likeCard(currentUser.id, currentCard.card_id);
if (!result.error) {
// Refresh user data to get updated liked_card_ids
handleApiResponse(result);
if (result && !result.error) {
// Update local card counts in cardQueue
setCardQueue((prev) =>
prev.map((c) =>
c.card_id === currentCard.card_id
? { ...c, count_likes: (c.count_likes || 0) + 1 }
: c
)
);
// Refresh user data for liked_card_ids
const userResult = await api.getUser(currentUser.id);
if (!userResult.error) setUser(userResult.result);
handleApiResponse(userResult);
if (!userResult.error && userResult.result) {
setUser(userResult.result);
}
}
return result;
}, [currentCard, chosenCard]);
}, [currentCard, chosenCard, handleApiResponse]);
const dislikeCard = useCallback(async () => {
if (!currentCard || !chosenCard) return;
hapticImpact('light');
const result = await api.dislikeCard(currentUser.id, currentCard.card_id);
if (!result.error) {
handleApiResponse(result);
if (result && !result.error) {
// Update local card counts in cardQueue
setCardQueue((prev) =>
prev.map((c) =>
c.card_id === currentCard.card_id
? { ...c, count_dislikes: (c.count_dislikes || 0) + 1 }
: c
)
);
// Refresh user data for disliked_card_ids
const userResult = await api.getUser(currentUser.id);
if (!userResult.error) setUser(userResult.result);
handleApiResponse(userResult);
if (!userResult.error && userResult.result) {
setUser(userResult.result);
}
}
return result;
}, [currentCard, chosenCard]);
// Toast helper
const showToast = useCallback((message, duration = 2500) => {
setToast(message);
setTimeout(() => setToast(null), duration);
}, []);
}, [currentCard, chosenCard, handleApiResponse]);
// Menu helpers
const openMenu = useCallback(() => {
hapticImpact('light');
setIsMenuOpen(true);
setMenuScreen('menu');
}, []);
const closeMenu = useCallback(() => {
hapticImpact('light');
setIsMenuOpen(false);
setMenuScreen('menu');
}, []);
const handleRetryAfterBan = useCallback(() => {
setIsBanned(false);
setIsLoading(true);
initApp();
}, [initApp]);
const value = {
// State
isLoading,
isLoadingCards,
user,
error,
isBanned,
currentCard,
chosenCard,
cardQueue,
@@ -169,12 +344,17 @@ export function AppProvider({ children }) {
loadCards,
likeCard,
dislikeCard,
getUserProfile,
handleApiResponse,
syncCardComments,
addCommentToCard,
openMenu,
closeMenu,
setMenuScreen,
setIsCommentsOpen,
showToast,
setError,
handleRetryAfterBan,
};
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;