diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx
index d684eaf..33d2e07 100644
--- a/src/app/[locale]/layout.tsx
+++ b/src/app/[locale]/layout.tsx
@@ -6,6 +6,7 @@ import { routing } from '@/i18n/routing';
import { ToastProvider } from '@/app/providers/ToastProvider';
import "../styles/globals.css";
import "../styles/global-styles.scss";
+import ActivityTracker from '../components/ActivityTracker';
export function generateStaticParams() {
return routing.locales.map((locale: any) => ({ locale }));
@@ -34,6 +35,7 @@ export default async function RootLayout({
{children}
+
diff --git a/src/app/components/ActivityTracker.tsx b/src/app/components/ActivityTracker.tsx
new file mode 100644
index 0000000..b5ebad5
--- /dev/null
+++ b/src/app/components/ActivityTracker.tsx
@@ -0,0 +1,107 @@
+// components/ActivityTracker.tsx
+'use client';
+
+import { useEffect, useRef, useCallback } from 'react';
+import { tokenLifeExtension } from '@/app/actions/auth';
+
+export default function ActivityTracker() {
+ const lastRefreshTime = useRef(Date.now());
+ const activityTimeoutRef = useRef(null);
+ const isRefreshing = useRef(false);
+
+ // Debounce функция
+ const debounce = useCallback((func: () => void, delay: number) => {
+ if (activityTimeoutRef.current) {
+ clearTimeout(activityTimeoutRef.current);
+ }
+
+ activityTimeoutRef.current = setTimeout(func, delay);
+ }, []);
+
+ // Ваша функция обновления токена (адаптированная для клиента)
+ const refreshTokenSilently = useCallback(async () => {
+ if (isRefreshing.current) return false;
+
+ isRefreshing.current = true;
+
+ try {
+ let response = tokenLifeExtension();
+
+ //@ts-ignore
+ return response.ok;
+ } catch (error) {
+ console.error('Silent token refresh failed:', error);
+ return false;
+ } finally {
+ isRefreshing.current = false;
+ }
+ }, []);
+
+ // Проверка необходимости обновления
+ const checkAndRefresh = useCallback(async () => {
+ const now = Date.now();
+ const timeSinceLastRefresh = now - lastRefreshTime.current;
+
+ // Обновляем не чаще чем раз в 10 минут
+ // и только если прошло больше 5 минут с последнего обновления
+ if (timeSinceLastRefresh > 5 * 60 * 1000 && timeSinceLastRefresh > 30000) {
+ const refreshed = await refreshTokenSilently();
+ if (refreshed) {
+ lastRefreshTime.current = now;
+ console.log('Token silently refreshed at', new Date().toISOString());
+ }
+ }
+ }, [refreshTokenSilently]);
+
+ // Обработчик активности
+ const handleActivity = useCallback(() => {
+ // Используем debounce чтобы не спамить запросами
+ // Ждем 30 секунд после последнего действия пользователя
+ debounce(checkAndRefresh, 30000);
+ }, [debounce, checkAndRefresh]);
+
+ useEffect(() => {
+ const events = [
+ 'mousedown', 'mousemove', 'wheel', 'keydown',
+ 'touchstart', 'touchend', 'scroll', 'click',
+ 'focus', 'blur', 'input', 'change'
+ ];
+
+ // Добавляем обработчики с passive: true для производительности
+ events.forEach(event => {
+ document.addEventListener(event, handleActivity, { passive: true });
+ });
+
+ // Также обновляем при возвращении на вкладку
+ const handleVisibilityChange = () => {
+ if (!document.hidden) {
+ handleActivity();
+ // Немедленно проверяем при возвращении на вкладку
+ setTimeout(checkAndRefresh, 1000);
+ }
+ };
+
+ document.addEventListener('visibilitychange', handleVisibilityChange);
+
+ // Периодическая проверка (каждые 2 минуты)
+ const intervalId = setInterval(checkAndRefresh, 2 * 60 * 1000);
+
+ // Инициализация - проверяем сразу если нужно
+ const initialCheck = setTimeout(checkAndRefresh, 10000);
+
+ return () => {
+ // Очистка
+ events.forEach(event => {
+ document.removeEventListener(event, handleActivity);
+ });
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
+ clearInterval(intervalId);
+ clearTimeout(initialCheck);
+ if (activityTimeoutRef.current) {
+ clearTimeout(activityTimeoutRef.current);
+ }
+ };
+ }, [handleActivity, checkAndRefresh]);
+
+ return null; // Не рендерит ничего
+}
\ No newline at end of file