changed reward on button

This commit is contained in:
SlimusMinus
2026-09-10 00:57:31 +03:00
parent 8839b9b2cc
commit c58a921d73
16 changed files with 1239 additions and 9 deletions

49
src/hooks/useProfile.js Normal file
View File

@@ -0,0 +1,49 @@
import { useState, useCallback } from "react";
import { useTranslation } from 'react-i18next';
import { API_URL } from "../config/authConstants";
import { apiFetch } from "../api/apiClient";
/**
* useProfile — хук для получения свежих данных профиля с бэка.
* Делает GET /api/v1/users/{id} и возвращает актуальные данные пользователя
* (включая аватар), которые могут устареть в локальном контексте, если
* профиль менялся с другого устройства.
*
* fetchProfile возвращает { success, data, error }.
*/
export function useProfile() {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const fetchProfile = useCallback(async (userId) => {
if (!userId) {
const message = t('profile.fetchMissingUserId');
setError(message);
return { success: false, data: null, error: message };
}
setLoading(true);
setError("");
try {
const res = await apiFetch(`${API_URL}/api/v1/users/${userId}`);
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.message || t('profile.fetchErrorWithStatus', { status: res.status }));
}
const user = await res.json();
return { success: true, data: user, error: null };
} catch (err) {
const message = err.message || t('profile.fetchError');
setError(message);
return { success: false, data: null, error: message };
} finally {
setLoading(false);
}
}, [t]);
return { fetchProfile, loading, error };
}

View File

@@ -0,0 +1,71 @@
import { useEffect, useRef, useState } from "react";
import type { RefObject } from "react";
export type ScrollDirection = "up" | "down";
/**
* useScrollDirection — отслеживает направление скролла контейнера `containerRef`
* и сообщает, когда тулбар должен быть скрыт.
*
* Логика (как в Telegram Web):
* - при скролле вниз тулбар прячется (hidden = true);
* - при скролле вверх (или подъезде в самое начало списка) — показывается;
* - есть порог `showThreshold` у вершины, чтобы тулбар появлялся не раньше,
* чем контент вернулся достаточно близко к началу.
*
* requestAnimationFrame + ref'ы: обработчик скролла НЕ триггерит ре-рендер
* на каждое событие — состояние меняется только когда скрытость реально
* пересекает границу, всё остальное живёт в ref'ах.
*/
export default function useScrollDirection<T extends HTMLElement>(
containerRef: RefObject<T | null>,
{ hideThreshold = 12, showThreshold = 88 }: { hideThreshold?: number; showThreshold?: number } = {},
): boolean {
const [hidden, setHidden] = useState(false);
const hiddenRef = useRef(false);
const lastYRef = useRef(0);
const rafRef = useRef<number | null>(null);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
lastYRef.current = el.scrollTop;
const onScroll = () => {
if (rafRef.current != null) return;
rafRef.current = requestAnimationFrame(() => {
rafRef.current = null;
const y = el.scrollTop;
const maxScroll = el.scrollHeight - el.clientHeight;
const isAtTop = y <= showThreshold;
const isAtBottom = maxScroll > 0 && y >= maxScroll - showThreshold;
// Без сенсорных/фрикционных возражений: крошечные рывки не считаем за жест
const dy = Math.abs(y - lastYRef.current);
let next = hiddenRef.current;
if (isAtTop || isAtBottom) {
next = false;
} else if (dy >= hideThreshold) {
next = y > lastYRef.current;
}
lastYRef.current = y;
if (next !== hiddenRef.current) {
hiddenRef.current = next;
setHidden(next);
}
});
};
el.addEventListener("scroll", onScroll, { passive: true });
return () => {
el.removeEventListener("scroll", onScroll);
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
};
}, [containerRef, hideThreshold, showThreshold]);
return hidden;
}