feat: add live refresh functionality and configuration status hooks; enhance UI components with new switch and refresh features

This commit is contained in:
Arunavo Ray
2025-05-24 10:24:25 +05:30
parent fc985f29df
commit 0890ed0bb8
14 changed files with 485 additions and 74 deletions

View File

@@ -0,0 +1,84 @@
import { useCallback, useEffect, useState } from 'react';
import { useAuth } from './useAuth';
import { apiRequest } from '@/lib/utils';
import type { ConfigApiResponse } from '@/types/config';
interface ConfigStatus {
isGitHubConfigured: boolean;
isGiteaConfigured: boolean;
isFullyConfigured: boolean;
isLoading: boolean;
error: string | null;
}
/**
* Hook to check if GitHub and Gitea are properly configured
* Returns configuration status and prevents unnecessary API calls when not configured
*/
export function useConfigStatus(): ConfigStatus {
const { user } = useAuth();
const [configStatus, setConfigStatus] = useState<ConfigStatus>({
isGitHubConfigured: false,
isGiteaConfigured: false,
isFullyConfigured: false,
isLoading: true,
error: null,
});
const checkConfiguration = useCallback(async () => {
if (!user?.id) {
setConfigStatus({
isGitHubConfigured: false,
isGiteaConfigured: false,
isFullyConfigured: false,
isLoading: false,
error: 'No user found',
});
return;
}
try {
setConfigStatus(prev => ({ ...prev, isLoading: true, error: null }));
const configResponse = await apiRequest<ConfigApiResponse>(
`/config?userId=${user.id}`,
{ method: 'GET' }
);
const isGitHubConfigured = !!(
configResponse?.githubConfig?.username &&
configResponse?.githubConfig?.token
);
const isGiteaConfigured = !!(
configResponse?.giteaConfig?.url &&
configResponse?.giteaConfig?.username &&
configResponse?.giteaConfig?.token
);
const isFullyConfigured = isGitHubConfigured && isGiteaConfigured;
setConfigStatus({
isGitHubConfigured,
isGiteaConfigured,
isFullyConfigured,
isLoading: false,
error: null,
});
} catch (error) {
setConfigStatus({
isGitHubConfigured: false,
isGiteaConfigured: false,
isFullyConfigured: false,
isLoading: false,
error: error instanceof Error ? error.message : 'Failed to check configuration',
});
}
}, [user?.id]);
useEffect(() => {
checkConfiguration();
}, [checkConfiguration]);
return configStatus;
}

102
src/hooks/useLiveRefresh.ts Normal file
View File

@@ -0,0 +1,102 @@
import * as React from "react";
import { useState, useEffect, createContext, useContext, useCallback, useRef } from "react";
import { usePageVisibility } from "./usePageVisibility";
import { useConfigStatus } from "./useConfigStatus";
interface LiveRefreshContextType {
isLiveEnabled: boolean;
toggleLive: () => void;
registerRefreshCallback: (callback: () => void) => () => void;
}
const LiveRefreshContext = createContext<LiveRefreshContextType | undefined>(undefined);
const LIVE_REFRESH_INTERVAL = 3000; // 3 seconds
const SESSION_STORAGE_KEY = 'gitea-mirror-live-refresh';
export function LiveRefreshProvider({ children }: { children: React.ReactNode }) {
const [isLiveEnabled, setIsLiveEnabled] = useState<boolean>(false);
const isPageVisible = usePageVisibility();
const { isFullyConfigured } = useConfigStatus();
const refreshCallbacksRef = useRef<Set<() => void>>(new Set());
const intervalRef = useRef<NodeJS.Timeout | null>(null);
// Load initial state from session storage
useEffect(() => {
const savedState = sessionStorage.getItem(SESSION_STORAGE_KEY);
if (savedState === 'true') {
setIsLiveEnabled(true);
}
}, []);
// Save state to session storage whenever it changes
useEffect(() => {
sessionStorage.setItem(SESSION_STORAGE_KEY, isLiveEnabled.toString());
}, [isLiveEnabled]);
// Execute all registered refresh callbacks
const executeRefreshCallbacks = useCallback(() => {
refreshCallbacksRef.current.forEach(callback => {
try {
callback();
} catch (error) {
console.error('Error executing refresh callback:', error);
}
});
}, []);
// Setup/cleanup the refresh interval
useEffect(() => {
// Clear existing interval
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
// Only set up interval if live is enabled, page is visible, and configuration is complete
if (isLiveEnabled && isPageVisible && isFullyConfigured) {
intervalRef.current = setInterval(executeRefreshCallbacks, LIVE_REFRESH_INTERVAL);
}
// Cleanup on unmount
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [isLiveEnabled, isPageVisible, isFullyConfigured, executeRefreshCallbacks]);
const toggleLive = useCallback(() => {
setIsLiveEnabled(prev => !prev);
}, []);
const registerRefreshCallback = useCallback((callback: () => void) => {
refreshCallbacksRef.current.add(callback);
// Return cleanup function
return () => {
refreshCallbacksRef.current.delete(callback);
};
}, []);
const contextValue = {
isLiveEnabled,
toggleLive,
registerRefreshCallback,
};
return React.createElement(
LiveRefreshContext.Provider,
{ value: contextValue },
children
);
}
export function useLiveRefresh() {
const context = useContext(LiveRefreshContext);
if (context === undefined) {
throw new Error("useLiveRefresh must be used within a LiveRefreshProvider");
}
return context;
}

View File

@@ -0,0 +1,28 @@
import { useEffect, useState } from 'react';
/**
* Hook to detect if the page/tab is currently visible
* Returns false when user switches to another tab or minimizes the window
*/
export function usePageVisibility(): boolean {
const [isVisible, setIsVisible] = useState<boolean>(true);
useEffect(() => {
const handleVisibilityChange = () => {
setIsVisible(!document.hidden);
};
// Set initial state
setIsVisible(!document.hidden);
// Listen for visibility changes
document.addEventListener('visibilitychange', handleVisibilityChange);
// Cleanup
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, []);
return isVisible;
}