@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.
- package/dist/index.mjs +2003 -882
- package/package.json +3 -1
- package/src/components/github-stars/github-api.ts +413 -0
- package/src/components/github-stars/hooks.ts +362 -0
- package/src/components/github-stars/index.tsx +215 -288
- package/src/components/github-stars/types.ts +146 -0
- package/src/components/github-stars/variants.tsx +380 -0
- package/src/components/lazy-component/index.tsx +567 -85
- package/src/components/virtual-list/index.tsx +6 -105
|
@@ -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
|
+
}
|