@moontra/moonui-pro 2.17.4 → 2.18.0

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,304 @@
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
+
58
+ const checkMilestones = useCallback((repos: GitHubRepository[]) => {
59
+ if (!onMilestoneReached) return
60
+
61
+ repos.forEach(repo => {
62
+ const previousStars = previousStarsRef.current.get(repo.full_name) || 0
63
+ const currentStars = repo.stargazers_count
64
+
65
+ milestones.forEach(milestone => {
66
+ if (previousStars < milestone && currentStars >= milestone) {
67
+ onMilestoneReached({
68
+ count: milestone,
69
+ reached: true,
70
+ date: new Date().toISOString(),
71
+ celebration: true,
72
+ })
73
+ }
74
+ })
75
+
76
+ previousStarsRef.current.set(repo.full_name, currentStars)
77
+ })
78
+ }, [milestones, onMilestoneReached])
79
+
80
+ const fetchData = useCallback(async () => {
81
+ try {
82
+ setLoading(true)
83
+ setError(null)
84
+
85
+ // Check rate limit first
86
+ try {
87
+ const rateLimit = await getRateLimitInfo(token)
88
+ setRateLimitInfo(rateLimit)
89
+
90
+ if (rateLimit.remaining < 10) {
91
+ console.warn(`Low GitHub API rate limit: ${rateLimit.remaining} requests remaining`)
92
+ }
93
+ } catch (error) {
94
+ console.error("Failed to fetch rate limit:", error)
95
+ }
96
+
97
+ let fetchedRepos: GitHubRepository[] = []
98
+
99
+ // Single repository mode
100
+ if (repository && username) {
101
+ const repo = await fetchRepository(username, repository, token)
102
+ fetchedRepos = [repo]
103
+ }
104
+ // Multiple specific repositories
105
+ else if (repositories && repositories.length > 0 && username) {
106
+ const repoPromises = repositories.map(repoName =>
107
+ fetchRepository(username, repoName, token)
108
+ )
109
+ fetchedRepos = await Promise.all(repoPromises)
110
+ }
111
+ // All user repositories
112
+ else if (username) {
113
+ fetchedRepos = await fetchUserRepositories(username, token, {
114
+ sort: sortBy,
115
+ per_page: 100,
116
+ })
117
+ }
118
+
119
+ // Sort repositories
120
+ fetchedRepos.sort((a, b) => {
121
+ switch (sortBy) {
122
+ case "stars":
123
+ return b.stargazers_count - a.stargazers_count
124
+ case "forks":
125
+ return b.forks_count - a.forks_count
126
+ case "updated":
127
+ return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
128
+ case "created":
129
+ return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
130
+ case "name":
131
+ return a.name.localeCompare(b.name)
132
+ case "issues":
133
+ return b.open_issues_count - a.open_issues_count
134
+ default:
135
+ return 0
136
+ }
137
+ })
138
+
139
+ // Limit results
140
+ if (maxItems && maxItems > 0) {
141
+ fetchedRepos = fetchedRepos.slice(0, maxItems)
142
+ }
143
+
144
+ // Fetch additional data for detailed view
145
+ const enhancedRepos = await Promise.all(
146
+ fetchedRepos.map(async repo => {
147
+ try {
148
+ const contributorsCount = await fetchContributorsCount(
149
+ repo.owner.login,
150
+ repo.name,
151
+ token
152
+ )
153
+ return { ...repo, contributors_count: contributorsCount }
154
+ } catch (error) {
155
+ console.error(`Failed to fetch contributors for ${repo.full_name}:`, error)
156
+ return repo
157
+ }
158
+ })
159
+ )
160
+
161
+ setRepos(enhancedRepos)
162
+
163
+ // Calculate statistics
164
+ const calculatedStats = calculateStats(enhancedRepos)
165
+ setStats(calculatedStats)
166
+
167
+ // Check milestones
168
+ checkMilestones(enhancedRepos)
169
+
170
+ // Notify data update
171
+ if (onDataUpdate) {
172
+ onDataUpdate(calculatedStats)
173
+ }
174
+
175
+ setLastUpdated(new Date())
176
+ } catch (err) {
177
+ const errorMessage = err instanceof Error ? err.message : "Failed to fetch data"
178
+ setError(errorMessage)
179
+ if (onError) {
180
+ onError(err instanceof Error ? err : new Error(errorMessage))
181
+ }
182
+ } finally {
183
+ setLoading(false)
184
+ }
185
+ }, [
186
+ username,
187
+ repository,
188
+ repositories,
189
+ token,
190
+ sortBy,
191
+ maxItems,
192
+ checkMilestones,
193
+ onDataUpdate,
194
+ onError,
195
+ ])
196
+
197
+ // Initial fetch
198
+ useEffect(() => {
199
+ fetchData()
200
+ }, [fetchData])
201
+
202
+ // Auto-refresh
203
+ useEffect(() => {
204
+ if (!autoRefresh) return
205
+
206
+ const scheduleRefresh = () => {
207
+ refreshTimeoutRef.current = setTimeout(() => {
208
+ fetchData()
209
+ scheduleRefresh()
210
+ }, refreshInterval)
211
+ }
212
+
213
+ scheduleRefresh()
214
+
215
+ return () => {
216
+ if (refreshTimeoutRef.current) {
217
+ clearTimeout(refreshTimeoutRef.current)
218
+ }
219
+ }
220
+ }, [autoRefresh, refreshInterval, fetchData])
221
+
222
+ const refresh = useCallback(() => {
223
+ clearCache()
224
+ return fetchData()
225
+ }, [fetchData])
226
+
227
+ return {
228
+ repos,
229
+ stats,
230
+ loading,
231
+ error,
232
+ rateLimitInfo,
233
+ lastUpdated,
234
+ refresh,
235
+ }
236
+ }
237
+
238
+ // Hook for star history
239
+ export function useStarHistory(
240
+ owner: string,
241
+ repo: string,
242
+ token?: string
243
+ ) {
244
+ const [history, setHistory] = useState<StarHistory[]>([])
245
+ const [loading, setLoading] = useState(true)
246
+ const [error, setError] = useState<string | null>(null)
247
+
248
+ useEffect(() => {
249
+ const fetchHistory = async () => {
250
+ try {
251
+ setLoading(true)
252
+ setError(null)
253
+ const data = await fetchStarHistory(owner, repo, token)
254
+ setHistory(data)
255
+ } catch (err) {
256
+ setError(err instanceof Error ? err.message : "Failed to fetch star history")
257
+ } finally {
258
+ setLoading(false)
259
+ }
260
+ }
261
+
262
+ if (owner && repo) {
263
+ fetchHistory()
264
+ }
265
+ }, [owner, repo, token])
266
+
267
+ return { history, loading, error }
268
+ }
269
+
270
+ // Hook for notifications
271
+ export function useGitHubNotifications(enabled: boolean = true) {
272
+ const [permission, setPermission] = useState<NotificationPermission>("default")
273
+
274
+ useEffect(() => {
275
+ if (!enabled || typeof window === "undefined" || !("Notification" in window)) {
276
+ return
277
+ }
278
+
279
+ setPermission(Notification.permission)
280
+
281
+ if (Notification.permission === "default") {
282
+ Notification.requestPermission().then(setPermission)
283
+ }
284
+ }, [enabled])
285
+
286
+ const notify = useCallback(
287
+ (title: string, options?: NotificationOptions) => {
288
+ if (!enabled || permission !== "granted") return
289
+
290
+ try {
291
+ new Notification(title, {
292
+ icon: "/icon-192x192.png",
293
+ badge: "/icon-192x192.png",
294
+ ...options,
295
+ })
296
+ } catch (error) {
297
+ console.error("Failed to show notification:", error)
298
+ }
299
+ },
300
+ [enabled, permission]
301
+ )
302
+
303
+ return { permission, notify }
304
+ }