@moontra/moonui-pro 2.17.5 → 2.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,362 @@
1
+ import { useState, useEffect, useCallback, useRef } from "react"
2
+ import {
3
+ GitHubRepository,
4
+ GitHubStats,
5
+ RateLimitInfo,
6
+ StarHistory,
7
+ Milestone,
8
+ } from "./types"
9
+ import {
10
+ fetchUserRepositories,
11
+ fetchRepository,
12
+ fetchContributorsCount,
13
+ fetchStarHistory,
14
+ calculateStats,
15
+ getRateLimitInfo,
16
+ clearCache,
17
+ } from "./github-api"
18
+
19
+ interface UseGitHubDataOptions {
20
+ username?: string
21
+ repository?: string
22
+ repositories?: string[]
23
+ token?: string
24
+ autoRefresh?: boolean
25
+ refreshInterval?: number
26
+ sortBy?: string
27
+ maxItems?: number
28
+ onError?: (error: Error) => void
29
+ onDataUpdate?: (stats: GitHubStats) => void
30
+ onMilestoneReached?: (milestone: Milestone) => void
31
+ milestones?: number[]
32
+ }
33
+
34
+ export function useGitHubData({
35
+ username,
36
+ repository,
37
+ repositories,
38
+ token,
39
+ autoRefresh = false,
40
+ refreshInterval = 300000,
41
+ sortBy = "stars",
42
+ maxItems,
43
+ onError,
44
+ onDataUpdate,
45
+ onMilestoneReached,
46
+ milestones = [10, 50, 100, 500, 1000, 5000, 10000],
47
+ }: UseGitHubDataOptions) {
48
+ const [repos, setRepos] = useState<GitHubRepository[]>([])
49
+ const [stats, setStats] = useState<GitHubStats | null>(null)
50
+ const [loading, setLoading] = useState(true)
51
+ const [error, setError] = useState<string | null>(null)
52
+ const [rateLimitInfo, setRateLimitInfo] = useState<RateLimitInfo | null>(null)
53
+ const [lastUpdated, setLastUpdated] = useState<Date | null>(null)
54
+
55
+ const refreshTimeoutRef = useRef<NodeJS.Timeout>()
56
+ const previousStarsRef = useRef<Map<string, number>>(new Map())
57
+ const errorCountRef = useRef<number>(0) // Hata sayısını takip et
58
+ const maxErrorCount = 2 // Maksimum hata sayısı
59
+
60
+ const checkMilestones = useCallback((repos: GitHubRepository[]) => {
61
+ if (!onMilestoneReached) return
62
+
63
+ repos.forEach(repo => {
64
+ const previousStars = previousStarsRef.current.get(repo.full_name) || 0
65
+ const currentStars = repo.stargazers_count
66
+
67
+ milestones.forEach(milestone => {
68
+ if (previousStars < milestone && currentStars >= milestone) {
69
+ onMilestoneReached({
70
+ count: milestone,
71
+ reached: true,
72
+ date: new Date().toISOString(),
73
+ celebration: true,
74
+ })
75
+ }
76
+ })
77
+
78
+ previousStarsRef.current.set(repo.full_name, currentStars)
79
+ })
80
+ }, [milestones, onMilestoneReached])
81
+
82
+ const fetchData = useCallback(async () => {
83
+ // Hata limiti aşıldıysa istek yapma
84
+ if (errorCountRef.current >= maxErrorCount) {
85
+ console.warn("Maximum error count reached. Stopping requests.")
86
+ setLoading(false)
87
+ setError("Maximum retry limit exceeded. Please check your configuration.")
88
+ return
89
+ }
90
+
91
+ // Gerekli bilgiler yoksa istek yapma
92
+ const hasValidInput =
93
+ (username && repository) || // Tek repository modu
94
+ (username && repositories && repositories.length > 0) || // Çoklu repository modu
95
+ (repositories && repositories.length > 0 && repositories.every(r => r.includes('/'))) || // Full path repositories
96
+ username // Kullanıcının tüm repositoryleri
97
+
98
+ if (!hasValidInput) {
99
+ console.warn("No valid input provided. Skipping API request.")
100
+ setLoading(false)
101
+ setError(null)
102
+ setRepos([])
103
+ setStats(null)
104
+ return
105
+ }
106
+
107
+ try {
108
+ setLoading(true)
109
+ setError(null)
110
+
111
+ // Check rate limit first
112
+ try {
113
+ const rateLimit = await getRateLimitInfo(token)
114
+ setRateLimitInfo(rateLimit)
115
+
116
+ if (rateLimit.remaining < 10) {
117
+ console.warn(`Low GitHub API rate limit: ${rateLimit.remaining} requests remaining`)
118
+ }
119
+ } catch (error) {
120
+ console.error("Failed to fetch rate limit:", error)
121
+ }
122
+
123
+ let fetchedRepos: GitHubRepository[] = []
124
+
125
+ // Single repository mode
126
+ if (repository && username) {
127
+ const repo = await fetchRepository(username, repository, token)
128
+ fetchedRepos = [repo]
129
+ }
130
+ // Multiple specific repositories with full paths
131
+ else if (repositories && repositories.length > 0 && repositories.every(r => r.includes('/'))) {
132
+ const repoPromises = repositories.map(fullPath => {
133
+ const [owner, name] = fullPath.split('/')
134
+ return fetchRepository(owner, name, token)
135
+ })
136
+ fetchedRepos = await Promise.all(repoPromises)
137
+ }
138
+ // Multiple specific repositories with username
139
+ else if (repositories && repositories.length > 0 && username) {
140
+ const repoPromises = repositories.map(repoName =>
141
+ fetchRepository(username, repoName, token)
142
+ )
143
+ fetchedRepos = await Promise.all(repoPromises)
144
+ }
145
+ // All user repositories
146
+ else if (username) {
147
+ fetchedRepos = await fetchUserRepositories(username, token, {
148
+ sort: sortBy,
149
+ per_page: 100,
150
+ })
151
+ }
152
+
153
+ // Sort repositories
154
+ fetchedRepos.sort((a, b) => {
155
+ switch (sortBy) {
156
+ case "stars":
157
+ return b.stargazers_count - a.stargazers_count
158
+ case "forks":
159
+ return b.forks_count - a.forks_count
160
+ case "updated":
161
+ return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
162
+ case "created":
163
+ return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
164
+ case "name":
165
+ return a.name.localeCompare(b.name)
166
+ case "issues":
167
+ return b.open_issues_count - a.open_issues_count
168
+ default:
169
+ return 0
170
+ }
171
+ })
172
+
173
+ // Limit results
174
+ if (maxItems && maxItems > 0) {
175
+ fetchedRepos = fetchedRepos.slice(0, maxItems)
176
+ }
177
+
178
+ // Fetch additional data for detailed view
179
+ const enhancedRepos = await Promise.all(
180
+ fetchedRepos.map(async repo => {
181
+ try {
182
+ const contributorsCount = await fetchContributorsCount(
183
+ repo.owner.login,
184
+ repo.name,
185
+ token
186
+ )
187
+ return { ...repo, contributors_count: contributorsCount }
188
+ } catch (error) {
189
+ console.error(`Failed to fetch contributors for ${repo.full_name}:`, error)
190
+ return repo
191
+ }
192
+ })
193
+ )
194
+
195
+ setRepos(enhancedRepos)
196
+
197
+ // Calculate statistics
198
+ const calculatedStats = calculateStats(enhancedRepos)
199
+ setStats(calculatedStats)
200
+
201
+ // Check milestones
202
+ checkMilestones(enhancedRepos)
203
+
204
+ // Notify data update
205
+ if (onDataUpdate) {
206
+ onDataUpdate(calculatedStats)
207
+ }
208
+
209
+ setLastUpdated(new Date())
210
+ // Başarılı olduğunda hata sayacını sıfırla
211
+ errorCountRef.current = 0
212
+ } catch (err) {
213
+ const errorMessage = err instanceof Error ? err.message : "Failed to fetch data"
214
+ setError(errorMessage)
215
+
216
+ // Hata sayacını artır
217
+ errorCountRef.current += 1
218
+ console.error(`GitHub API error (${errorCountRef.current}/${maxErrorCount}):`, errorMessage)
219
+
220
+ if (onError) {
221
+ onError(err instanceof Error ? err : new Error(errorMessage))
222
+ }
223
+ } finally {
224
+ setLoading(false)
225
+ }
226
+ }, [
227
+ username,
228
+ repository,
229
+ repositories,
230
+ token,
231
+ sortBy,
232
+ maxItems,
233
+ checkMilestones,
234
+ onDataUpdate,
235
+ onError,
236
+ ])
237
+
238
+ // Initial fetch
239
+ useEffect(() => {
240
+ // Sadece geçerli input varsa fetch yap
241
+ const hasValidInput =
242
+ (username && repository) ||
243
+ (username && repositories && repositories.length > 0) ||
244
+ (repositories && repositories.length > 0 && repositories.every(r => r.includes('/'))) ||
245
+ username
246
+
247
+ if (hasValidInput) {
248
+ fetchData()
249
+ } else {
250
+ setLoading(false)
251
+ }
252
+ }, [fetchData])
253
+
254
+ // Auto-refresh
255
+ useEffect(() => {
256
+ if (!autoRefresh) return
257
+
258
+ const scheduleRefresh = () => {
259
+ refreshTimeoutRef.current = setTimeout(() => {
260
+ fetchData()
261
+ scheduleRefresh()
262
+ }, refreshInterval)
263
+ }
264
+
265
+ scheduleRefresh()
266
+
267
+ return () => {
268
+ if (refreshTimeoutRef.current) {
269
+ clearTimeout(refreshTimeoutRef.current)
270
+ }
271
+ }
272
+ }, [autoRefresh, refreshInterval, fetchData])
273
+
274
+ const refresh = useCallback(() => {
275
+ // Hata limiti aşıldıysa refresh yapma
276
+ if (errorCountRef.current >= maxErrorCount) {
277
+ console.warn("Cannot refresh: maximum error count reached")
278
+ return Promise.resolve()
279
+ }
280
+
281
+ clearCache()
282
+ return fetchData()
283
+ }, [fetchData])
284
+
285
+ return {
286
+ repos,
287
+ stats,
288
+ loading,
289
+ error,
290
+ rateLimitInfo,
291
+ lastUpdated,
292
+ refresh,
293
+ }
294
+ }
295
+
296
+ // Hook for star history
297
+ export function useStarHistory(
298
+ owner: string,
299
+ repo: string,
300
+ token?: string
301
+ ) {
302
+ const [history, setHistory] = useState<StarHistory[]>([])
303
+ const [loading, setLoading] = useState(true)
304
+ const [error, setError] = useState<string | null>(null)
305
+
306
+ useEffect(() => {
307
+ const fetchHistory = async () => {
308
+ try {
309
+ setLoading(true)
310
+ setError(null)
311
+ const data = await fetchStarHistory(owner, repo, token)
312
+ setHistory(data)
313
+ } catch (err) {
314
+ setError(err instanceof Error ? err.message : "Failed to fetch star history")
315
+ } finally {
316
+ setLoading(false)
317
+ }
318
+ }
319
+
320
+ if (owner && repo) {
321
+ fetchHistory()
322
+ }
323
+ }, [owner, repo, token])
324
+
325
+ return { history, loading, error }
326
+ }
327
+
328
+ // Hook for notifications
329
+ export function useGitHubNotifications(enabled: boolean = true) {
330
+ const [permission, setPermission] = useState<NotificationPermission>("default")
331
+
332
+ useEffect(() => {
333
+ if (!enabled || typeof window === "undefined" || !("Notification" in window)) {
334
+ return
335
+ }
336
+
337
+ setPermission(Notification.permission)
338
+
339
+ if (Notification.permission === "default") {
340
+ Notification.requestPermission().then(setPermission)
341
+ }
342
+ }, [enabled])
343
+
344
+ const notify = useCallback(
345
+ (title: string, options?: NotificationOptions) => {
346
+ if (!enabled || permission !== "granted") return
347
+
348
+ try {
349
+ new Notification(title, {
350
+ icon: "/icon-192x192.png",
351
+ badge: "/icon-192x192.png",
352
+ ...options,
353
+ })
354
+ } catch (error) {
355
+ console.error("Failed to show notification:", error)
356
+ }
357
+ },
358
+ [enabled, permission]
359
+ )
360
+
361
+ return { permission, notify }
362
+ }