UNPKG

@moontra/moonui-pro

Version:

Premium React components for MoonUI - Advanced UI library with 50+ pro components including performance, interactive, and gesture components

516 lines (458 loc) 15.9 kB
import { useState, useEffect, useCallback, useRef } from "react" import { GitHubRepository, GitHubStats, RateLimitInfo, StarHistory, Milestone, } from "./types" import { fetchUserRepositories, fetchRepository, fetchContributorsCount, fetchStarHistory, calculateStats, getRateLimitInfo, clearCache, } from "./github-api" interface UseGitHubDataOptions { username?: string repository?: string repositories?: string[] token?: string autoRefresh?: boolean refreshInterval?: number sortBy?: string maxItems?: number onError?: (error: Error) => void onDataUpdate?: (stats: GitHubStats) => void onMilestoneReached?: (milestone: Milestone) => void milestones?: number[] // For docs mode optimizations docsMode?: boolean mockDataFallback?: boolean forceMockData?: boolean // Force mock data instead of API } export function useGitHubData({ username, repository, repositories, token, autoRefresh = false, refreshInterval = 300000, sortBy = "stars", maxItems, onError, onDataUpdate, onMilestoneReached, milestones = [10, 50, 100, 500, 1000, 5000, 10000], docsMode = false, mockDataFallback = true, forceMockData = false, }: UseGitHubDataOptions) { // Docs mode detection const isDocsMode = docsMode || (typeof window !== "undefined" && (window.location.pathname.includes("/docs/") || window.location.pathname.includes("/components/"))) // Disable autoRefresh in docs mode const effectiveAutoRefresh = isDocsMode ? false : autoRefresh const [repos, setRepos] = useState<GitHubRepository[]>([]) const [stats, setStats] = useState<GitHubStats | null>(null) const [loading, setLoading] = useState(true) const [error, setError] = useState<string | null>(null) const [rateLimitInfo, setRateLimitInfo] = useState<RateLimitInfo | null>(null) const [lastUpdated, setLastUpdated] = useState<Date | null>(null) const refreshTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined) const previousStarsRef = useRef<Map<string, number>>(new Map()) const errorCountRef = useRef<number>(0) // Hata sayısını takip et const maxErrorCount = isDocsMode ? 1 : 2 // Fewer retries in docs mode const hasInitialFetchedRef = useRef(false) // İlk fetch yapıldı mı? const docsDataCacheRef = useRef<GitHubRepository[] | null>(null) // Docs mode cache // Store checkMilestones function as ref - this way it won't be recreated on every render const milestonesRef = useRef(milestones) const onMilestoneReachedRef = useRef(onMilestoneReached) // Ref'leri güncelle useEffect(() => { milestonesRef.current = milestones }, [milestones]) useEffect(() => { onMilestoneReachedRef.current = onMilestoneReached }, [onMilestoneReached]) const checkMilestones = useCallback((repos: GitHubRepository[]) => { if (!onMilestoneReachedRef.current) return repos.forEach(repo => { const previousStars = previousStarsRef.current.get(repo.full_name) || 0 const currentStars = repo.stargazers_count milestonesRef.current.forEach(milestone => { if (previousStars < milestone && currentStars >= milestone) { onMilestoneReachedRef.current?.({ count: milestone, reached: true, date: new Date().toISOString(), celebration: true, }) } }) previousStarsRef.current.set(repo.full_name, currentStars) }) }, []) // Boş dependency array - fonksiyon asla yeniden oluşturulmaz const fetchData = useCallback(async () => { // If forceMockData is true, always return mock data if (forceMockData) { console.log("[Mock Mode] Using mock data") const mockData = getMockGitHubData(username, repository, repositories) setRepos(mockData) const calculatedStats = calculateStats(mockData) setStats(calculatedStats) setError(null) setLoading(false) return } // Docs modunda ve daha önce fetch yapıldıysa, cache'den döndür if (isDocsMode && hasInitialFetchedRef.current && docsDataCacheRef.current) { console.log("[Docs Mode] Returning cached data, skipping API request") setRepos(docsDataCacheRef.current) const calculatedStats = calculateStats(docsDataCacheRef.current) setStats(calculatedStats) setLoading(false) return } // Hata limiti aşıldıysa istek yapma if (errorCountRef.current >= maxErrorCount) { console.warn("Maximum error count reached. Stopping requests.") // Show mock data if in docs mode and mockDataFallback is active if (isDocsMode && mockDataFallback) { const mockData = getMockGitHubData(username, repository, repositories) setRepos(mockData) const calculatedStats = calculateStats(mockData) setStats(calculatedStats) setError(null) } else { setError("Maximum retry limit exceeded. Please check your configuration.") } setLoading(false) return } // Don't make requests if required info is missing const hasValidInput = (username && repository) || // Tek repository modu (username && repositories && repositories.length > 0) || // Çoklu repository modu (repositories && repositories.length > 0 && repositories.every(r => r.includes('/'))) || // Full path repositories username // Kullanıcının tüm repositoryleri if (!hasValidInput) { console.warn("No valid input provided. Skipping API request.") setLoading(false) setError(null) setRepos([]) setStats(null) return } try { setLoading(true) setError(null) // Check rate limit first try { const rateLimit = await getRateLimitInfo(token) setRateLimitInfo(rateLimit) if (rateLimit.remaining < 10) { console.warn(`Low GitHub API rate limit: ${rateLimit.remaining} requests remaining`) } } catch (error) { console.error("Failed to fetch rate limit:", error) } let fetchedRepos: GitHubRepository[] = [] // Single repository mode if (repository && username) { const repo = await fetchRepository(username, repository, token) fetchedRepos = [repo] } // Multiple specific repositories with full paths else if (repositories && repositories.length > 0 && repositories.every(r => r.includes('/'))) { const repoPromises = repositories.map(fullPath => { const [owner, name] = fullPath.split('/') return fetchRepository(owner, name, token) }) fetchedRepos = await Promise.all(repoPromises) } // Multiple specific repositories with username else if (repositories && repositories.length > 0 && username) { const repoPromises = repositories.map(repoName => fetchRepository(username, repoName, token) ) fetchedRepos = await Promise.all(repoPromises) } // All user repositories else if (username) { fetchedRepos = await fetchUserRepositories(username, token, { sort: sortBy, per_page: 100, }) } // Sort repositories fetchedRepos.sort((a, b) => { switch (sortBy) { case "stars": return b.stargazers_count - a.stargazers_count case "forks": return b.forks_count - a.forks_count case "updated": return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime() case "created": return new Date(b.created_at).getTime() - new Date(a.created_at).getTime() case "name": return a.name.localeCompare(b.name) case "issues": return b.open_issues_count - a.open_issues_count default: return 0 } }) // Limit results if (maxItems && maxItems > 0) { fetchedRepos = fetchedRepos.slice(0, maxItems) } // Fetch additional data for detailed view const enhancedRepos = await Promise.all( fetchedRepos.map(async repo => { try { const contributorsCount = await fetchContributorsCount( repo.owner.login, repo.name, token ) return { ...repo, contributors_count: contributorsCount } } catch (error) { console.error(`Failed to fetch contributors for ${repo.full_name}:`, error) return repo } }) ) setRepos(enhancedRepos) // Calculate statistics const calculatedStats = calculateStats(enhancedRepos) setStats(calculatedStats) // Check milestones checkMilestones(enhancedRepos) // Notify data update if (onDataUpdate) { onDataUpdate(calculatedStats) } setLastUpdated(new Date()) // Reset error counter on success errorCountRef.current = 0 // Cache successful data in docs mode if (isDocsMode) { hasInitialFetchedRef.current = true docsDataCacheRef.current = enhancedRepos } } catch (err) { const errorMessage = err instanceof Error ? err.message : "Failed to fetch data" setError(errorMessage) // Hata sayacını artır errorCountRef.current += 1 console.error(`GitHub API error (${errorCountRef.current}/${maxErrorCount}):`, errorMessage) // Show mock data if in docs mode and mockDataFallback is active if (isDocsMode && mockDataFallback && !hasInitialFetchedRef.current) { console.warn("[Docs Mode] API failed, using mock data") const mockData = getMockGitHubData(username, repository, repositories) setRepos(mockData) const calculatedStats = calculateStats(mockData) setStats(calculatedStats) setError(null) hasInitialFetchedRef.current = true docsDataCacheRef.current = mockData } else if (onError) { onError(err instanceof Error ? err : new Error(errorMessage)) } } finally { setLoading(false) } }, [ username, repository, repositories?.join(','), // Convert array to string to keep reference stable token, sortBy, maxItems, checkMilestones, // Now stable onDataUpdate, onError, isDocsMode, mockDataFallback, forceMockData, ]) // Initial fetch useEffect(() => { // Only fetch if there's valid input const hasValidInput = (username && repository) || (username && repositories && repositories.length > 0) || (repositories && repositories.length > 0 && repositories.every(r => r.includes('/'))) || username if (hasValidInput) { fetchData() } else { setLoading(false) } }, [fetchData]) // Auto-refresh useEffect(() => { // Completely disable auto-refresh in docs mode if (!effectiveAutoRefresh) return const scheduleRefresh = () => { refreshTimeoutRef.current = setTimeout(() => { fetchData() scheduleRefresh() }, refreshInterval) } scheduleRefresh() return () => { if (refreshTimeoutRef.current) { clearTimeout(refreshTimeoutRef.current) } } }, [effectiveAutoRefresh, refreshInterval, fetchData]) const refresh = useCallback(() => { // Limit refresh in docs mode if (isDocsMode && hasInitialFetchedRef.current) { console.warn("[Docs Mode] Refresh disabled after initial fetch") return Promise.resolve() } // Hata limiti aşıldıysa refresh yapma if (errorCountRef.current >= maxErrorCount) { console.warn("Cannot refresh: maximum error count reached") return Promise.resolve() } clearCache() return fetchData() }, [fetchData, isDocsMode]) return { repos, stats, loading, error, rateLimitInfo, lastUpdated, refresh, } } // Hook for star history export function useStarHistory( owner: string, repo: string, token?: string ) { const [history, setHistory] = useState<StarHistory[]>([]) const [loading, setLoading] = useState(true) const [error, setError] = useState<string | null>(null) useEffect(() => { const fetchHistory = async () => { try { setLoading(true) setError(null) const data = await fetchStarHistory(owner, repo, token) setHistory(data) } catch (err) { setError(err instanceof Error ? err.message : "Failed to fetch star history") } finally { setLoading(false) } } if (owner && repo) { fetchHistory() } }, [owner, repo, token]) return { history, loading, error } } // Hook for notifications export function useGitHubNotifications(enabled: boolean = true) { const [permission, setPermission] = useState<NotificationPermission>("default") useEffect(() => { if (!enabled || typeof window === "undefined" || !("Notification" in window)) { return } setPermission(Notification.permission) if (Notification.permission === "default") { Notification.requestPermission().then(setPermission) } }, [enabled]) const notify = useCallback( (title: string, options?: NotificationOptions) => { if (!enabled || permission !== "granted") return try { new Notification(title, { icon: "/icon-192x192.png", badge: "/icon-192x192.png", ...options, }) } catch (error) { console.error("Failed to show notification:", error) } }, [enabled, permission] ) return { permission, notify } } // Mock data generator for docs mode function getMockGitHubData( username?: string, repository?: string, repositories?: string[] ): GitHubRepository[] { const defaultRepos: GitHubRepository[] = [ { id: 1, name: repository || "awesome-project", full_name: `${username || "moonui"}/${repository || "awesome-project"}`, description: "An amazing open source project with great features", html_url: `https://github.com/${username || "moonui"}/${repository || "awesome-project"}`, homepage: "https://awesome-project.dev", stargazers_count: 12453, watchers_count: 543, forks_count: 2341, language: "TypeScript", topics: ["react", "ui", "components", "typescript"], created_at: "2022-01-15T10:30:00Z", updated_at: new Date().toISOString(), pushed_at: new Date().toISOString(), size: 4567, open_issues_count: 23, license: { key: "mit", name: "MIT License", spdx_id: "MIT", url: "https://api.github.com/licenses/mit", }, owner: { login: username || "moonui", avatar_url: `https://github.com/${username || "moonui"}.png`, html_url: `https://github.com/${username || "moonui"}`, type: "Organization", }, contributors_count: 89, }, ] if (repositories && repositories.length > 0) { return repositories.map((repo, index) => { const [owner, name] = repo.includes('/') ? repo.split('/') : [username || "moonui", repo] return { ...defaultRepos[0], id: index + 1, name: name, full_name: `${owner}/${name}`, html_url: `https://github.com/${owner}/${name}`, stargazers_count: Math.floor(Math.random() * 20000) + 1000, forks_count: Math.floor(Math.random() * 3000) + 100, watchers_count: Math.floor(Math.random() * 1000) + 50, language: ["TypeScript", "JavaScript", "Python", "Go", "Rust"][index % 5], owner: { login: owner, avatar_url: `https://github.com/${owner}.png`, html_url: `https://github.com/${owner}`, type: "Organization", }, } }) } return defaultRepos }