@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
text/typescript
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
}