@moontra/moonui-pro 2.17.5 → 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.
- package/dist/index.mjs +1965 -882
- package/package.json +3 -1
- package/src/components/github-stars/github-api.ts +413 -0
- package/src/components/github-stars/hooks.ts +304 -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,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
|
+
}
|