@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.
@@ -1,170 +1,124 @@
1
1
  "use client"
2
2
 
3
- import React, { useState, useEffect } from "react"
4
- import { motion, AnimatePresence } from "framer-motion"
3
+ import React, { useMemo } from "react"
5
4
  import { Card, CardContent } from "../ui/card"
6
5
  import { Button } from "../ui/button"
7
- import { MoonUIBadgePro as Badge } from "../ui/badge"
8
6
  import { MoonUISkeletonPro as Skeleton } from "../ui/skeleton"
9
7
  import { cn } from "../../lib/utils"
10
- import { Star, GitFork, Eye, Users, ExternalLink, Github, Lock, Sparkles, RefreshCw } from "lucide-react"
8
+ import { Lock, Sparkles, RefreshCw, Github, Download, FileJson, FileSpreadsheet } from "lucide-react"
11
9
  import { useSubscription } from "../../hooks/use-subscription"
12
-
13
- interface GitHubRepository {
14
- id: number
15
- name: string
16
- full_name: string
17
- description: string | null
18
- html_url: string
19
- homepage: string | null
20
- stargazers_count: number
21
- watchers_count: number
22
- forks_count: number
23
- language: string | null
24
- topics: string[]
25
- created_at: string
26
- updated_at: string
27
- owner: {
28
- login: string
29
- avatar_url: string
30
- html_url: string
31
- }
32
- }
33
-
34
- interface GitHubStarsProps {
35
- username: string
36
- repositories?: string[]
37
- showDescription?: boolean
38
- showTopics?: boolean
39
- showStats?: boolean
40
- showOwner?: boolean
41
- sortBy?: "stars" | "forks" | "updated" | "created"
42
- layout?: "grid" | "list"
43
- maxItems?: number
44
- autoRefresh?: boolean
45
- refreshInterval?: number
46
- className?: string
47
- onRepositoryClick?: (repo: GitHubRepository) => void
48
- }
10
+ import { useGitHubData, useGitHubNotifications } from "./hooks"
11
+ import { exportData, exportAsCSV } from "./github-api"
12
+ import {
13
+ MinimalVariant,
14
+ CompactVariant,
15
+ CardVariant,
16
+ DetailedVariant,
17
+ } from "./variants"
18
+ import type { GitHubStarsProps } from "./types"
19
+ import { motion, AnimatePresence } from "framer-motion"
20
+ import confetti from "canvas-confetti"
49
21
 
50
22
  const GitHubStarsInternal: React.FC<GitHubStarsProps> = ({
51
- username,
23
+ username = "",
24
+ repository,
52
25
  repositories,
26
+ token,
27
+ variant = "card",
28
+ layout = "grid",
53
29
  showDescription = true,
54
30
  showTopics = true,
55
31
  showStats = true,
56
32
  showOwner = true,
33
+ showLanguage = true,
34
+ showActivity = false,
35
+ showTrending = false,
36
+ showMilestones = false,
37
+ showComparison = false,
38
+ showHistory = false,
57
39
  sortBy = "stars",
58
- layout = "grid",
59
40
  maxItems = 6,
60
41
  autoRefresh = false,
61
- refreshInterval = 300000, // 5 minutes
42
+ refreshInterval = 300000,
43
+ enableNotifications = false,
44
+ enableExport = true,
45
+ enableAnalytics = false,
46
+ cacheEnabled = true,
47
+ cacheDuration = 300000,
48
+ animation = "fade",
49
+ animationDuration = 0.3,
50
+ staggerDelay = 0.05,
51
+ milestones = [10, 50, 100, 500, 1000, 5000, 10000],
52
+ celebrateAt = [100, 1000, 10000],
53
+ onRepositoryClick,
54
+ onStarClick,
55
+ onMilestoneReached,
56
+ onDataUpdate,
57
+ onError,
62
58
  className,
63
- onRepositoryClick
59
+ theme = "auto",
60
+ size = "md",
61
+ customColors,
64
62
  }) => {
65
- const [repos, setRepos] = useState<GitHubRepository[]>([])
66
- const [loading, setLoading] = useState(true)
67
- const [error, setError] = useState<string | null>(null)
68
- const [lastUpdated, setLastUpdated] = useState<Date | null>(null)
69
-
70
- const fetchRepositories = async () => {
71
- try {
72
- setLoading(true)
73
- setError(null)
74
-
75
- let url = `https://api.github.com/users/${username}/repos?sort=${sortBy}&per_page=100`
76
-
77
- const response = await fetch(url)
78
- if (!response.ok) {
79
- throw new Error(`GitHub API error: ${response.status}`)
80
- }
81
-
82
- let data: GitHubRepository[] = await response.json()
83
-
84
- // Filter specific repositories if provided
85
- if (repositories && repositories.length > 0) {
86
- data = data.filter(repo => repositories.includes(repo.name))
87
- }
88
-
89
- // Sort repositories
90
- data.sort((a, b) => {
91
- switch (sortBy) {
92
- case "stars":
93
- return b.stargazers_count - a.stargazers_count
94
- case "forks":
95
- return b.forks_count - a.forks_count
96
- case "updated":
97
- return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
98
- case "created":
99
- return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
100
- default:
101
- return 0
102
- }
103
- })
63
+ const { notify } = useGitHubNotifications(enableNotifications)
104
64
 
105
- // Limit results
106
- if (maxItems) {
107
- data = data.slice(0, maxItems)
65
+ const {
66
+ repos,
67
+ stats,
68
+ loading,
69
+ error,
70
+ rateLimitInfo,
71
+ lastUpdated,
72
+ refresh,
73
+ } = useGitHubData({
74
+ username,
75
+ repository,
76
+ repositories,
77
+ token,
78
+ autoRefresh,
79
+ refreshInterval,
80
+ sortBy,
81
+ maxItems,
82
+ onError,
83
+ onDataUpdate,
84
+ onMilestoneReached: (milestone) => {
85
+ // Handle milestone reached
86
+ if (celebrateAt?.includes(milestone.count)) {
87
+ // Trigger celebration animation
88
+ confetti({
89
+ particleCount: 100,
90
+ spread: 70,
91
+ origin: { y: 0.6 },
92
+ })
93
+
94
+ // Show notification
95
+ notify(`🎉 Milestone Reached!`, {
96
+ body: `${milestone.count} stars achieved!`,
97
+ tag: `milestone-${milestone.count}`,
98
+ })
108
99
  }
100
+
101
+ onMilestoneReached?.(milestone)
102
+ },
103
+ milestones,
104
+ })
109
105
 
110
- setRepos(data)
111
- setLastUpdated(new Date())
112
- } catch (err) {
113
- setError(err instanceof Error ? err.message : "Failed to fetch repositories")
114
- } finally {
115
- setLoading(false)
116
- }
117
- }
118
-
119
- useEffect(() => {
120
- fetchRepositories()
121
- }, [username, repositories, sortBy, maxItems])
122
-
123
- useEffect(() => {
124
- if (!autoRefresh) return
125
-
126
- const interval = setInterval(fetchRepositories, refreshInterval)
127
- return () => clearInterval(interval)
128
- }, [autoRefresh, refreshInterval])
129
-
130
- const formatNumber = (num: number): string => {
131
- if (num >= 1000) {
132
- return (num / 1000).toFixed(1) + "k"
133
- }
134
- return num.toString()
135
- }
136
-
137
- const formatDate = (dateString: string): string => {
138
- const date = new Date(dateString)
139
- return date.toLocaleDateString("en-US", {
140
- year: "numeric",
141
- month: "short",
142
- day: "numeric"
143
- })
144
- }
106
+ const handleExport = (format: "json" | "csv") => {
107
+ if (!enableExport) return
145
108
 
146
- const getLanguageColor = (language: string | null): string => {
147
- const colors: Record<string, string> = {
148
- JavaScript: "#f7df1e",
149
- TypeScript: "#3178c6",
150
- Python: "#3776ab",
151
- Java: "#ed8b00",
152
- "C++": "#00599c",
153
- "C#": "#239120",
154
- Go: "#00add8",
155
- Rust: "#000000",
156
- Swift: "#fa7343",
157
- Kotlin: "#7f52ff",
158
- PHP: "#777bb4",
159
- Ruby: "#cc342d",
160
- HTML: "#e34f26",
161
- CSS: "#1572b6",
162
- Vue: "#4fc08d",
163
- React: "#61dafb"
109
+ const timestamp = new Date().toISOString().split("T")[0]
110
+
111
+ if (format === "json") {
112
+ exportData(
113
+ { repositories: repos, statistics: stats, exportDate: new Date() },
114
+ `github-stars-${username}-${timestamp}.json`
115
+ )
116
+ } else {
117
+ exportAsCSV(repos, `github-stars-${username}-${timestamp}.csv`)
164
118
  }
165
- return colors[language || ""] || "#6b7280"
166
119
  }
167
120
 
121
+ // Loading state
168
122
  if (loading) {
169
123
  return (
170
124
  <Card className={cn("w-full", className)}>
@@ -202,6 +156,7 @@ const GitHubStarsInternal: React.FC<GitHubStarsProps> = ({
202
156
  )
203
157
  }
204
158
 
159
+ // Error state
205
160
  if (error) {
206
161
  return (
207
162
  <Card className={cn("w-full", className)}>
@@ -211,8 +166,13 @@ const GitHubStarsInternal: React.FC<GitHubStarsProps> = ({
211
166
  <Github className="h-12 w-12 mx-auto mb-2" />
212
167
  <h3 className="font-semibold">Failed to load repositories</h3>
213
168
  <p className="text-sm text-muted-foreground">{error}</p>
169
+ {rateLimitInfo && rateLimitInfo.remaining === 0 && (
170
+ <p className="text-xs text-muted-foreground mt-2">
171
+ Rate limit will reset at {new Date(rateLimitInfo.reset).toLocaleTimeString()}
172
+ </p>
173
+ )}
214
174
  </div>
215
- <Button onClick={fetchRepositories} variant="outline">
175
+ <Button onClick={refresh} variant="outline">
216
176
  <RefreshCw className="h-4 w-4 mr-2" />
217
177
  Try Again
218
178
  </Button>
@@ -222,154 +182,122 @@ const GitHubStarsInternal: React.FC<GitHubStarsProps> = ({
222
182
  )
223
183
  }
224
184
 
225
- return (
226
- <Card className={cn("w-full", className)}>
227
- <CardContent className="p-6">
228
- <div className="space-y-6">
229
- {/* Header */}
230
- <div className="flex items-center justify-between">
231
- <div className="flex items-center gap-3">
232
- <Github className="h-6 w-6" />
233
- <div>
234
- <h3 className="font-semibold text-lg">{username}'s Repositories</h3>
235
- {lastUpdated && (
236
- <p className="text-sm text-muted-foreground">
237
- Last updated: {lastUpdated.toLocaleTimeString()}
238
- </p>
239
- )}
240
- </div>
241
- </div>
242
-
243
- <div className="flex items-center gap-2">
244
- <Badge variant="outline">
245
- {repos.length} repos
246
- </Badge>
247
- {autoRefresh && (
248
- <Badge variant="secondary">
249
- Auto-refresh
250
- </Badge>
251
- )}
252
- <Button onClick={fetchRepositories} variant="outline" size="sm">
253
- <RefreshCw className="h-4 w-4" />
254
- </Button>
255
- </div>
256
- </div>
257
-
258
- {/* Repository Grid/List */}
259
- <div className={cn(
260
- layout === "grid"
261
- ? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
262
- : "space-y-4"
263
- )}>
264
- <AnimatePresence>
265
- {repos.map((repo, index) => (
266
- <motion.div
267
- key={repo.id}
268
- initial={{ opacity: 0, y: 20 }}
269
- animate={{ opacity: 1, y: 0 }}
270
- exit={{ opacity: 0, y: -20 }}
271
- transition={{ delay: index * 0.1 }}
272
- >
273
- <Card
274
- className="h-full hover:shadow-md transition-shadow cursor-pointer group"
275
- onClick={() => onRepositoryClick?.(repo)}
276
- >
277
- <CardContent className="p-4">
278
- <div className="space-y-3">
279
- {/* Repository Name */}
280
- <div className="flex items-start justify-between">
281
- <div className="flex-1 min-w-0">
282
- <h4 className="font-semibold text-sm group-hover:text-primary transition-colors">
283
- {repo.name}
284
- </h4>
285
- {showOwner && (
286
- <p className="text-xs text-muted-foreground truncate">
287
- {repo.owner.login}
288
- </p>
289
- )}
290
- </div>
291
- <Button
292
- variant="ghost"
293
- size="sm"
294
- className="opacity-0 group-hover:opacity-100 transition-opacity p-1 h-auto"
295
- onClick={(e) => {
296
- e.stopPropagation()
297
- window.open(repo.html_url, '_blank')
298
- }}
299
- >
300
- <ExternalLink className="h-3 w-3" />
301
- </Button>
302
- </div>
303
-
304
- {/* Description */}
305
- {showDescription && repo.description && (
306
- <p className="text-sm text-muted-foreground line-clamp-2">
307
- {repo.description}
308
- </p>
309
- )}
185
+ // Render based on variant
186
+ const renderVariant = () => {
187
+ const baseProps = {
188
+ repos,
189
+ stats,
190
+ loading,
191
+ className,
192
+ onRepositoryClick,
193
+ }
310
194
 
311
- {/* Topics */}
312
- {showTopics && repo.topics.length > 0 && (
313
- <div className="flex flex-wrap gap-1">
314
- {repo.topics.slice(0, 3).map((topic) => (
315
- <Badge key={topic} variant="secondary" className="text-xs">
316
- {topic}
317
- </Badge>
318
- ))}
319
- {repo.topics.length > 3 && (
320
- <Badge variant="outline" className="text-xs">
321
- +{repo.topics.length - 3}
322
- </Badge>
323
- )}
324
- </div>
325
- )}
195
+ switch (variant) {
196
+ case "minimal":
197
+ return <MinimalVariant {...baseProps} />
198
+ case "compact":
199
+ return <CompactVariant {...baseProps} />
200
+ case "detailed":
201
+ return (
202
+ <DetailedVariant
203
+ {...baseProps}
204
+ onExport={() => {
205
+ // Show export dropdown
206
+ const dropdown = document.createElement("div")
207
+ dropdown.className = "absolute z-50 mt-2 w-48 rounded-md shadow-lg bg-popover border"
208
+ dropdown.innerHTML = `
209
+ <div class="py-1">
210
+ <button class="w-full text-left px-4 py-2 text-sm hover:bg-accent" data-format="json">
211
+ Export as JSON
212
+ </button>
213
+ <button class="w-full text-left px-4 py-2 text-sm hover:bg-accent" data-format="csv">
214
+ Export as CSV
215
+ </button>
216
+ </div>
217
+ `
218
+
219
+ dropdown.addEventListener("click", (e) => {
220
+ const target = e.target as HTMLElement
221
+ const format = target.getAttribute("data-format")
222
+ if (format === "json" || format === "csv") {
223
+ handleExport(format)
224
+ dropdown.remove()
225
+ }
226
+ })
227
+
228
+ document.body.appendChild(dropdown)
229
+
230
+ // Position dropdown
231
+ const rect = (e.target as HTMLElement).getBoundingClientRect()
232
+ dropdown.style.top = `${rect.bottom + window.scrollY}px`
233
+ dropdown.style.left = `${rect.left + window.scrollX}px`
234
+
235
+ // Remove on outside click
236
+ setTimeout(() => {
237
+ document.addEventListener("click", () => dropdown.remove(), { once: true })
238
+ }, 0)
239
+ }}
240
+ />
241
+ )
242
+ case "card":
243
+ default:
244
+ return <CardVariant {...baseProps} />
245
+ }
246
+ }
326
247
 
327
- {/* Stats */}
328
- {showStats && (
329
- <div className="flex items-center gap-4 text-sm text-muted-foreground">
330
- {repo.language && (
331
- <div className="flex items-center gap-1">
332
- <div
333
- className="w-3 h-3 rounded-full"
334
- style={{ backgroundColor: getLanguageColor(repo.language) }}
335
- />
336
- <span className="text-xs">{repo.language}</span>
337
- </div>
338
- )}
339
-
340
- <div className="flex items-center gap-1">
341
- <Star className="h-3 w-3" />
342
- <span className="text-xs">{formatNumber(repo.stargazers_count)}</span>
343
- </div>
344
-
345
- <div className="flex items-center gap-1">
346
- <GitFork className="h-3 w-3" />
347
- <span className="text-xs">{formatNumber(repo.forks_count)}</span>
348
- </div>
349
- </div>
350
- )}
248
+ // Apply animation wrapper
249
+ const animationVariants = {
250
+ bounce: {
251
+ initial: { scale: 0.9, opacity: 0 },
252
+ animate: { scale: 1, opacity: 1 },
253
+ transition: { type: "spring", stiffness: 300, damping: 20 },
254
+ },
255
+ pulse: {
256
+ initial: { scale: 0.95, opacity: 0 },
257
+ animate: { scale: 1, opacity: 1 },
258
+ transition: { duration: animationDuration },
259
+ },
260
+ fade: {
261
+ initial: { opacity: 0 },
262
+ animate: { opacity: 1 },
263
+ transition: { duration: animationDuration },
264
+ },
265
+ scale: {
266
+ initial: { scale: 0, opacity: 0 },
267
+ animate: { scale: 1, opacity: 1 },
268
+ transition: { duration: animationDuration },
269
+ },
270
+ slide: {
271
+ initial: { x: -20, opacity: 0 },
272
+ animate: { x: 0, opacity: 1 },
273
+ transition: { duration: animationDuration },
274
+ },
275
+ none: {
276
+ initial: {},
277
+ animate: {},
278
+ transition: {},
279
+ },
280
+ }
351
281
 
352
- {/* Updated Date */}
353
- <div className="text-xs text-muted-foreground">
354
- Updated {formatDate(repo.updated_at)}
355
- </div>
356
- </div>
357
- </CardContent>
358
- </Card>
359
- </motion.div>
360
- ))}
361
- </AnimatePresence>
362
- </div>
282
+ const selectedAnimation = animationVariants[animation]
363
283
 
364
- {repos.length === 0 && (
365
- <div className="text-center py-8">
366
- <Github className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
367
- <p className="text-muted-foreground">No repositories found</p>
368
- </div>
369
- )}
284
+ return (
285
+ <motion.div
286
+ {...selectedAnimation}
287
+ className={cn("w-full", className)}
288
+ >
289
+ {renderVariant()}
290
+
291
+ {/* Rate limit warning */}
292
+ {rateLimitInfo && rateLimitInfo.remaining < 10 && (
293
+ <div className="mt-4 p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-md text-sm">
294
+ <p className="text-yellow-800 dark:text-yellow-200">
295
+ ⚠️ Low API rate limit: {rateLimitInfo.remaining} requests remaining.
296
+ {token ? "" : " Consider adding a GitHub token for higher limits."}
297
+ </p>
370
298
  </div>
371
- </CardContent>
372
- </Card>
299
+ )}
300
+ </motion.div>
373
301
  )
374
302
  }
375
303
 
@@ -377,8 +305,6 @@ export const GitHubStars: React.FC<GitHubStarsProps> = ({ className, ...props })
377
305
  // Check if we're in docs mode or have pro access
378
306
  const { hasProAccess, isLoading } = useSubscription()
379
307
 
380
- // In docs mode, always show the component
381
-
382
308
  // If not in docs mode and no pro access, show upgrade prompt
383
309
  if (!isLoading && !hasProAccess) {
384
310
  return (
@@ -409,4 +335,5 @@ export const GitHubStars: React.FC<GitHubStarsProps> = ({ className, ...props })
409
335
  return <GitHubStarsInternal className={className} {...props} />
410
336
  }
411
337
 
412
- export type { GitHubRepository, GitHubStarsProps }
338
+ export type { GitHubRepository, GitHubStarsProps } from "./types"
339
+ export { LANGUAGE_COLORS } from "./github-api"