@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.
- package/dist/index.mjs +2719 -742
- package/package.json +3 -1
- package/src/components/calendar-pro/index.tsx +129 -24
- 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/memory-efficient-data/index.tsx +730 -66
- package/src/components/virtual-list/index.tsx +335 -35
- package/dist/index.d.ts +0 -2798
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moontra/moonui-pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.18.0",
|
|
4
4
|
"description": "Premium React components for MoonUI - Advanced UI library with 50+ pro components including performance, interactive, and gesture components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -92,6 +92,7 @@
|
|
|
92
92
|
"@radix-ui/react-toast": "^1.2.14",
|
|
93
93
|
"@radix-ui/react-tooltip": "^1.2.7",
|
|
94
94
|
"@tanstack/react-table": "^8.20.5",
|
|
95
|
+
"canvas-confetti": "^1.9.3",
|
|
95
96
|
"class-variance-authority": "^0.7.0",
|
|
96
97
|
"clsx": "^2.1.1",
|
|
97
98
|
"date-fns": "^3.6.0",
|
|
@@ -114,6 +115,7 @@
|
|
|
114
115
|
"@testing-library/jest-dom": "^6.6.3",
|
|
115
116
|
"@testing-library/react": "^16.3.0",
|
|
116
117
|
"@testing-library/user-event": "^14.6.1",
|
|
118
|
+
"@types/canvas-confetti": "^1.9.0",
|
|
117
119
|
"@types/jest": "^30.0.0",
|
|
118
120
|
"@types/node": "^18.16.0",
|
|
119
121
|
"@types/react": "^19.0.0",
|
|
@@ -271,7 +271,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel,
|
|
|
271
271
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from '../ui/command'
|
|
272
272
|
import { motion, AnimatePresence, LayoutGroup, useMotionValue, useTransform, animate } from 'framer-motion'
|
|
273
273
|
import { format, startOfMonth, endOfMonth, startOfWeek, endOfWeek, addDays, addWeeks, addMonths, addYears, subMonths, subYears, isSameMonth, isSameDay, isToday, isBefore, isAfter, differenceInDays, differenceInWeeks, differenceInMonths, parseISO, startOfDay, endOfDay, isWithinInterval, eachDayOfInterval, getDay, setHours, setMinutes, startOfYear, endOfYear, eachMonthOfInterval, getDaysInMonth, getWeek, startOfISOWeek, endOfISOWeek, addHours, subHours, isSameHour, differenceInHours, differenceInMinutes, addMinutes, subMinutes, isSameMinute, setSeconds, setMilliseconds, subDays, subWeeks } from 'date-fns'
|
|
274
|
-
|
|
274
|
+
// Removed react-beautiful-dnd imports - using HTML5 drag & drop instead
|
|
275
275
|
import { Calendar as CalendarBase } from '../ui/calendar'
|
|
276
276
|
import { HexColorPicker } from 'react-colorful'
|
|
277
277
|
|
|
@@ -441,6 +441,9 @@ export const CalendarPro = React.forwardRef<HTMLDivElement, CalendarProProps>(({
|
|
|
441
441
|
theme = 'system',
|
|
442
442
|
...props
|
|
443
443
|
}, ref) => {
|
|
444
|
+
// Drag state
|
|
445
|
+
const [draggedEventId, setDraggedEventId] = useState<string | null>(null)
|
|
446
|
+
const [dragOverInfo, setDragOverInfo] = useState<{ date: Date; hour?: number } | null>(null)
|
|
444
447
|
const [internalSidebarCollapsed, setInternalSidebarCollapsed] = useState(false)
|
|
445
448
|
const sidebarCollapsed = controlledSidebarCollapsed ?? internalSidebarCollapsed
|
|
446
449
|
|
|
@@ -617,29 +620,91 @@ export const CalendarPro = React.forwardRef<HTMLDivElement, CalendarProProps>(({
|
|
|
617
620
|
}, [selectedEvent, onEventDelete])
|
|
618
621
|
|
|
619
622
|
// Handle drag start
|
|
620
|
-
const handleDragStart = useCallback((event: CalendarEvent) => {
|
|
623
|
+
const handleDragStart = useCallback((e: React.DragEvent, event: CalendarEvent) => {
|
|
621
624
|
if (!allowEventDragging) return
|
|
625
|
+
|
|
626
|
+
setDraggedEventId(event.id)
|
|
622
627
|
setIsDragging(true)
|
|
623
|
-
|
|
628
|
+
|
|
629
|
+
// Store event data in dataTransfer
|
|
630
|
+
e.dataTransfer.effectAllowed = 'move'
|
|
631
|
+
e.dataTransfer.setData('text/plain', event.id)
|
|
632
|
+
|
|
633
|
+
// Create drag image with preserved dimensions
|
|
634
|
+
const dragImage = e.currentTarget.cloneNode(true) as HTMLElement
|
|
635
|
+
const originalRect = e.currentTarget.getBoundingClientRect()
|
|
636
|
+
dragImage.style.opacity = '0.8'
|
|
637
|
+
dragImage.style.position = 'absolute'
|
|
638
|
+
dragImage.style.top = '-1000px'
|
|
639
|
+
dragImage.style.width = `${originalRect.width}px`
|
|
640
|
+
dragImage.style.height = `${originalRect.height}px`
|
|
641
|
+
dragImage.style.boxSizing = 'border-box'
|
|
642
|
+
document.body.appendChild(dragImage)
|
|
643
|
+
e.dataTransfer.setDragImage(dragImage, e.nativeEvent.offsetX, e.nativeEvent.offsetY)
|
|
644
|
+
setTimeout(() => document.body.removeChild(dragImage), 0)
|
|
624
645
|
}, [allowEventDragging])
|
|
625
646
|
|
|
626
|
-
// Handle drag
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
647
|
+
// Handle drag over
|
|
648
|
+
const handleDragOver = useCallback((e: React.DragEvent, date: Date, hour?: number) => {
|
|
649
|
+
e.preventDefault()
|
|
650
|
+
e.dataTransfer.dropEffect = 'move'
|
|
651
|
+
setDragOverInfo({ date, hour })
|
|
652
|
+
}, [])
|
|
653
|
+
|
|
654
|
+
// Handle drag leave
|
|
655
|
+
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
|
656
|
+
// Only clear if we're leaving the drop zone entirely
|
|
657
|
+
const rect = e.currentTarget.getBoundingClientRect()
|
|
658
|
+
const x = e.clientX
|
|
659
|
+
const y = e.clientY
|
|
660
|
+
|
|
661
|
+
if (x < rect.left || x >= rect.right || y < rect.top || y >= rect.bottom) {
|
|
662
|
+
setDragOverInfo(null)
|
|
663
|
+
}
|
|
664
|
+
}, [])
|
|
665
|
+
|
|
666
|
+
// Handle drop
|
|
667
|
+
const handleDrop = useCallback((e: React.DragEvent, date: Date, hour?: number) => {
|
|
668
|
+
e.preventDefault()
|
|
669
|
+
|
|
670
|
+
const eventId = e.dataTransfer.getData('text/plain')
|
|
671
|
+
const draggedEvent = events.find(ev => ev.id === eventId)
|
|
672
|
+
|
|
673
|
+
if (!draggedEvent || !onEventDrop) return
|
|
674
|
+
|
|
675
|
+
// Calculate new start and end times
|
|
676
|
+
let newStart: Date
|
|
677
|
+
let newEnd: Date
|
|
631
678
|
|
|
632
|
-
if (
|
|
679
|
+
if (hour !== undefined) {
|
|
680
|
+
// Dropped on a specific hour slot
|
|
681
|
+
newStart = setHours(setMinutes(date, 0), hour)
|
|
682
|
+
} else {
|
|
683
|
+
// Dropped on a day (keep original time)
|
|
684
|
+
const originalHours = draggedEvent.start.getHours()
|
|
685
|
+
const originalMinutes = draggedEvent.start.getMinutes()
|
|
686
|
+
newStart = setHours(setMinutes(date, originalMinutes), originalHours)
|
|
687
|
+
}
|
|
633
688
|
|
|
634
|
-
//
|
|
635
|
-
// This is a simplified version - you'd need to implement proper date calculation
|
|
636
|
-
// based on the view and drop position
|
|
637
|
-
const newStart = new Date(result.destination.droppableId)
|
|
689
|
+
// Keep the same duration
|
|
638
690
|
const duration = differenceInMinutes(draggedEvent.end, draggedEvent.start)
|
|
639
|
-
|
|
691
|
+
newEnd = addMinutes(newStart, duration)
|
|
640
692
|
|
|
693
|
+
// Update event
|
|
641
694
|
onEventDrop(draggedEvent.id, newStart, newEnd)
|
|
642
|
-
|
|
695
|
+
|
|
696
|
+
// Reset drag state
|
|
697
|
+
setDraggedEventId(null)
|
|
698
|
+
setDragOverInfo(null)
|
|
699
|
+
setIsDragging(false)
|
|
700
|
+
}, [events, onEventDrop])
|
|
701
|
+
|
|
702
|
+
// Handle drag end (cleanup)
|
|
703
|
+
const handleDragEnd = useCallback(() => {
|
|
704
|
+
setDraggedEventId(null)
|
|
705
|
+
setDragOverInfo(null)
|
|
706
|
+
setIsDragging(false)
|
|
707
|
+
}, [])
|
|
643
708
|
|
|
644
709
|
// Export calendar
|
|
645
710
|
const exportCalendar = useCallback((format: 'ics' | 'csv' | 'json') => {
|
|
@@ -721,7 +786,15 @@ END:VCALENDAR`
|
|
|
721
786
|
<div className="flex-1 overflow-auto">
|
|
722
787
|
<div className="min-h-full">
|
|
723
788
|
{/* All day events */}
|
|
724
|
-
<div
|
|
789
|
+
<div
|
|
790
|
+
className={cn(
|
|
791
|
+
"border-b p-2",
|
|
792
|
+
dragOverInfo?.date && !dragOverInfo?.hour && isSameDay(dragOverInfo.date, currentDate) && "bg-primary/10"
|
|
793
|
+
)}
|
|
794
|
+
onDragOver={(e) => handleDragOver(e, currentDate)}
|
|
795
|
+
onDragLeave={handleDragLeave}
|
|
796
|
+
onDrop={(e) => handleDrop(e, currentDate)}
|
|
797
|
+
>
|
|
725
798
|
<div className="text-xs text-muted-foreground mb-1">All Day</div>
|
|
726
799
|
<div className="space-y-1">
|
|
727
800
|
{dayEvents
|
|
@@ -729,10 +802,14 @@ END:VCALENDAR`
|
|
|
729
802
|
.map(event => (
|
|
730
803
|
<div
|
|
731
804
|
key={event.id}
|
|
805
|
+
draggable={allowEventDragging}
|
|
806
|
+
onDragStart={(e) => handleDragStart(e, event)}
|
|
807
|
+
onDragEnd={handleDragEnd}
|
|
732
808
|
className="p-2 rounded text-xs cursor-pointer hover:opacity-80"
|
|
733
809
|
style={{
|
|
734
810
|
backgroundColor: event.color || eventColors[event.category || ''] || '#3b82f6',
|
|
735
|
-
color: '#ffffff'
|
|
811
|
+
color: '#ffffff',
|
|
812
|
+
opacity: draggedEventId === event.id ? 0.5 : 1
|
|
736
813
|
}}
|
|
737
814
|
onClick={(e) => handleEventClick(event, e)}
|
|
738
815
|
>
|
|
@@ -750,7 +827,13 @@ END:VCALENDAR`
|
|
|
750
827
|
{format(setHours(new Date(), hour), timeFormat === '12h' ? 'h a' : 'HH:00')}
|
|
751
828
|
</div>
|
|
752
829
|
<div
|
|
753
|
-
className=
|
|
830
|
+
className={cn(
|
|
831
|
+
"flex-1 relative border-l cursor-pointer hover:bg-muted/20",
|
|
832
|
+
dragOverInfo?.date && dragOverInfo?.hour === hour && isSameDay(dragOverInfo.date, currentDate) && "bg-primary/10"
|
|
833
|
+
)}
|
|
834
|
+
onDragOver={(e) => handleDragOver(e, currentDate, hour)}
|
|
835
|
+
onDragLeave={handleDragLeave}
|
|
836
|
+
onDrop={(e) => handleDrop(e, currentDate, hour)}
|
|
754
837
|
onClick={() => {
|
|
755
838
|
if (allowEventCreation) {
|
|
756
839
|
const clickedTime = setHours(setMinutes(currentDate, 0), hour)
|
|
@@ -783,13 +866,18 @@ END:VCALENDAR`
|
|
|
783
866
|
return (
|
|
784
867
|
<div
|
|
785
868
|
key={event.id}
|
|
869
|
+
draggable={allowEventDragging}
|
|
870
|
+
onDragStart={(e) => handleDragStart(e, event)}
|
|
871
|
+
onDragEnd={handleDragEnd}
|
|
786
872
|
className="absolute left-0 right-0 mx-1 p-1 rounded text-xs cursor-pointer hover:opacity-80 overflow-hidden"
|
|
787
873
|
style={{
|
|
788
874
|
top: `${top}px`,
|
|
789
875
|
height: `${height}px`,
|
|
790
876
|
backgroundColor: event.color || eventColors[event.category || ''] || '#3b82f6',
|
|
791
877
|
color: '#ffffff',
|
|
792
|
-
zIndex: 10
|
|
878
|
+
zIndex: 10,
|
|
879
|
+
opacity: draggedEventId === event.id ? 0.5 : 1,
|
|
880
|
+
cursor: allowEventDragging ? 'move' : 'pointer'
|
|
793
881
|
}}
|
|
794
882
|
onClick={(e) => {
|
|
795
883
|
e.stopPropagation()
|
|
@@ -860,14 +948,17 @@ END:VCALENDAR`
|
|
|
860
948
|
const dayEvents = eventsInView.filter(event =>
|
|
861
949
|
isSameDay(new Date(event.start), day) && !event.allDay
|
|
862
950
|
)
|
|
863
|
-
|
|
864
951
|
return (
|
|
865
952
|
<div
|
|
866
953
|
key={day.toISOString()}
|
|
867
954
|
className={cn(
|
|
868
955
|
"flex-1 relative border-l border-b cursor-pointer hover:bg-muted/20",
|
|
869
|
-
isToday(day) && "bg-primary/5"
|
|
956
|
+
isToday(day) && "bg-primary/5",
|
|
957
|
+
dragOverInfo?.date && dragOverInfo?.hour === hour && isSameDay(dragOverInfo.date, day) && "bg-primary/10"
|
|
870
958
|
)}
|
|
959
|
+
onDragOver={(e) => handleDragOver(e, day, hour)}
|
|
960
|
+
onDragLeave={handleDragLeave}
|
|
961
|
+
onDrop={(e) => handleDrop(e, day, hour)}
|
|
871
962
|
onClick={() => {
|
|
872
963
|
if (allowEventCreation) {
|
|
873
964
|
const clickedTime = setHours(setMinutes(day, 0), hour)
|
|
@@ -898,13 +989,18 @@ END:VCALENDAR`
|
|
|
898
989
|
return (
|
|
899
990
|
<div
|
|
900
991
|
key={event.id}
|
|
992
|
+
draggable={allowEventDragging}
|
|
993
|
+
onDragStart={(e) => handleDragStart(e, event)}
|
|
994
|
+
onDragEnd={handleDragEnd}
|
|
901
995
|
className="absolute left-0 right-0 mx-1 p-1 rounded text-xs cursor-pointer hover:opacity-80 overflow-hidden"
|
|
902
996
|
style={{
|
|
903
997
|
top: `${top}px`,
|
|
904
998
|
height: `${height}px`,
|
|
905
999
|
backgroundColor: event.color || eventColors[event.category || ''] || '#3b82f6',
|
|
906
1000
|
color: '#ffffff',
|
|
907
|
-
zIndex: 10
|
|
1001
|
+
zIndex: 10,
|
|
1002
|
+
opacity: draggedEventId === event.id ? 0.5 : 1,
|
|
1003
|
+
cursor: allowEventDragging ? 'move' : 'pointer'
|
|
908
1004
|
}}
|
|
909
1005
|
onClick={(e) => {
|
|
910
1006
|
e.stopPropagation()
|
|
@@ -971,8 +1067,12 @@ END:VCALENDAR`
|
|
|
971
1067
|
"min-h-[100px] p-2 border rounded-lg cursor-pointer transition-colors",
|
|
972
1068
|
!isCurrentMonth && "opacity-50",
|
|
973
1069
|
isToday(day) && "bg-primary/10 border-primary",
|
|
974
|
-
"hover:bg-muted/50"
|
|
1070
|
+
"hover:bg-muted/50",
|
|
1071
|
+
dragOverInfo?.date && !dragOverInfo?.hour && isSameDay(dragOverInfo.date, day) && "bg-primary/20 border-primary"
|
|
975
1072
|
)}
|
|
1073
|
+
onDragOver={(e) => handleDragOver(e, day)}
|
|
1074
|
+
onDragLeave={handleDragLeave}
|
|
1075
|
+
onDrop={(e) => handleDrop(e, day)}
|
|
976
1076
|
onClick={() => handleDateSelect(day)}
|
|
977
1077
|
>
|
|
978
1078
|
<div className={cn(
|
|
@@ -986,10 +1086,15 @@ END:VCALENDAR`
|
|
|
986
1086
|
return (
|
|
987
1087
|
<motion.div
|
|
988
1088
|
key={event.id}
|
|
1089
|
+
draggable={allowEventDragging}
|
|
1090
|
+
onDragStart={(e) => handleDragStart(e, event)}
|
|
1091
|
+
onDragEnd={handleDragEnd}
|
|
989
1092
|
className="text-xs p-1 rounded cursor-pointer truncate"
|
|
990
1093
|
style={{
|
|
991
1094
|
backgroundColor: event.color || eventColors[event.category || ''] || '#3b82f6',
|
|
992
|
-
color: '#ffffff'
|
|
1095
|
+
color: '#ffffff',
|
|
1096
|
+
opacity: draggedEventId === event.id ? 0.5 : 1,
|
|
1097
|
+
cursor: allowEventDragging ? 'move' : 'pointer'
|
|
993
1098
|
}}
|
|
994
1099
|
whileHover={{ scale: 1.02 }}
|
|
995
1100
|
whileTap={{ scale: 0.98 }}
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { GitHubRepository, GitHubStats, GitHubActivity, LanguageStats, RateLimitInfo, StarHistory } from "./types"
|
|
2
|
+
|
|
3
|
+
// Cache management
|
|
4
|
+
const cache = new Map<string, { data: any; timestamp: number; expiresAt: number }>()
|
|
5
|
+
|
|
6
|
+
// Language colors
|
|
7
|
+
export const LANGUAGE_COLORS: Record<string, string> = {
|
|
8
|
+
JavaScript: "#f7df1e",
|
|
9
|
+
TypeScript: "#3178c6",
|
|
10
|
+
Python: "#3776ab",
|
|
11
|
+
Java: "#ed8b00",
|
|
12
|
+
"C++": "#00599c",
|
|
13
|
+
"C#": "#239120",
|
|
14
|
+
Go: "#00add8",
|
|
15
|
+
Rust: "#000000",
|
|
16
|
+
Swift: "#fa7343",
|
|
17
|
+
Kotlin: "#7f52ff",
|
|
18
|
+
PHP: "#777bb4",
|
|
19
|
+
Ruby: "#cc342d",
|
|
20
|
+
HTML: "#e34f26",
|
|
21
|
+
CSS: "#1572b6",
|
|
22
|
+
Vue: "#4fc08d",
|
|
23
|
+
React: "#61dafb",
|
|
24
|
+
Shell: "#89e051",
|
|
25
|
+
Dart: "#0175c2",
|
|
26
|
+
Elixir: "#6e4a7e",
|
|
27
|
+
Scala: "#c22d40",
|
|
28
|
+
R: "#198ce7",
|
|
29
|
+
Julia: "#9558b2",
|
|
30
|
+
Lua: "#000080",
|
|
31
|
+
Perl: "#39457e",
|
|
32
|
+
Haskell: "#5e5086",
|
|
33
|
+
Clojure: "#db5855",
|
|
34
|
+
Erlang: "#b83998",
|
|
35
|
+
Objective_C: "#438eff",
|
|
36
|
+
// Add more as needed
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// API base URL
|
|
40
|
+
const API_BASE = "https://api.github.com"
|
|
41
|
+
|
|
42
|
+
// Helper to make authenticated requests
|
|
43
|
+
async function githubFetch(url: string, token?: string): Promise<Response> {
|
|
44
|
+
const headers: HeadersInit = {
|
|
45
|
+
Accept: "application/vnd.github.v3+json",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (token) {
|
|
49
|
+
headers.Authorization = `token ${token}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const response = await fetch(url, { headers })
|
|
53
|
+
|
|
54
|
+
if (response.status === 403 && response.headers.get("X-RateLimit-Remaining") === "0") {
|
|
55
|
+
const resetTime = parseInt(response.headers.get("X-RateLimit-Reset") || "0") * 1000
|
|
56
|
+
const resetDate = new Date(resetTime)
|
|
57
|
+
throw new Error(`GitHub API rate limit exceeded. Resets at ${resetDate.toLocaleTimeString()}`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return response
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Get rate limit info
|
|
68
|
+
export async function getRateLimitInfo(token?: string): Promise<RateLimitInfo> {
|
|
69
|
+
const cacheKey = `rate-limit-${token || "public"}`
|
|
70
|
+
const cached = cache.get(cacheKey)
|
|
71
|
+
|
|
72
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
73
|
+
return cached.data
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const response = await githubFetch(`${API_BASE}/rate_limit`, token)
|
|
77
|
+
const data = await response.json()
|
|
78
|
+
|
|
79
|
+
const rateLimitInfo: RateLimitInfo = {
|
|
80
|
+
limit: data.rate.limit,
|
|
81
|
+
remaining: data.rate.remaining,
|
|
82
|
+
reset: data.rate.reset * 1000,
|
|
83
|
+
used: data.rate.used,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
cache.set(cacheKey, {
|
|
87
|
+
data: rateLimitInfo,
|
|
88
|
+
timestamp: Date.now(),
|
|
89
|
+
expiresAt: Date.now() + 60000, // Cache for 1 minute
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
return rateLimitInfo
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Fetch user repositories
|
|
96
|
+
export async function fetchUserRepositories(
|
|
97
|
+
username: string,
|
|
98
|
+
token?: string,
|
|
99
|
+
options?: {
|
|
100
|
+
sort?: string
|
|
101
|
+
per_page?: number
|
|
102
|
+
page?: number
|
|
103
|
+
}
|
|
104
|
+
): Promise<GitHubRepository[]> {
|
|
105
|
+
const params = new URLSearchParams({
|
|
106
|
+
sort: options?.sort || "updated",
|
|
107
|
+
per_page: String(options?.per_page || 100),
|
|
108
|
+
page: String(options?.page || 1),
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const cacheKey = `repos-${username}-${params.toString()}`
|
|
112
|
+
const cached = cache.get(cacheKey)
|
|
113
|
+
|
|
114
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
115
|
+
return cached.data
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const response = await githubFetch(`${API_BASE}/users/${username}/repos?${params}`, token)
|
|
119
|
+
const repos = await response.json()
|
|
120
|
+
|
|
121
|
+
cache.set(cacheKey, {
|
|
122
|
+
data: repos,
|
|
123
|
+
timestamp: Date.now(),
|
|
124
|
+
expiresAt: Date.now() + 300000, // Cache for 5 minutes
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
return repos
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Fetch single repository
|
|
131
|
+
export async function fetchRepository(
|
|
132
|
+
owner: string,
|
|
133
|
+
repo: string,
|
|
134
|
+
token?: string
|
|
135
|
+
): Promise<GitHubRepository> {
|
|
136
|
+
const cacheKey = `repo-${owner}-${repo}`
|
|
137
|
+
const cached = cache.get(cacheKey)
|
|
138
|
+
|
|
139
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
140
|
+
return cached.data
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const response = await githubFetch(`${API_BASE}/repos/${owner}/${repo}`, token)
|
|
144
|
+
const repository = await response.json()
|
|
145
|
+
|
|
146
|
+
cache.set(cacheKey, {
|
|
147
|
+
data: repository,
|
|
148
|
+
timestamp: Date.now(),
|
|
149
|
+
expiresAt: Date.now() + 300000, // Cache for 5 minutes
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
return repository
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Fetch repository contributors count
|
|
156
|
+
export async function fetchContributorsCount(
|
|
157
|
+
owner: string,
|
|
158
|
+
repo: string,
|
|
159
|
+
token?: string
|
|
160
|
+
): Promise<number> {
|
|
161
|
+
const cacheKey = `contributors-${owner}-${repo}`
|
|
162
|
+
const cached = cache.get(cacheKey)
|
|
163
|
+
|
|
164
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
165
|
+
return cached.data
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const response = await githubFetch(
|
|
170
|
+
`${API_BASE}/repos/${owner}/${repo}/contributors?per_page=1&anon=true`,
|
|
171
|
+
token
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
// Get total count from Link header
|
|
175
|
+
const linkHeader = response.headers.get("Link")
|
|
176
|
+
if (linkHeader) {
|
|
177
|
+
const match = linkHeader.match(/page=(\d+)>; rel="last"/)
|
|
178
|
+
if (match) {
|
|
179
|
+
const count = parseInt(match[1])
|
|
180
|
+
cache.set(cacheKey, {
|
|
181
|
+
data: count,
|
|
182
|
+
timestamp: Date.now(),
|
|
183
|
+
expiresAt: Date.now() + 3600000, // Cache for 1 hour
|
|
184
|
+
})
|
|
185
|
+
return count
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// If no pagination, count the results
|
|
190
|
+
const contributors = await response.json()
|
|
191
|
+
const count = contributors.length
|
|
192
|
+
|
|
193
|
+
cache.set(cacheKey, {
|
|
194
|
+
data: count,
|
|
195
|
+
timestamp: Date.now(),
|
|
196
|
+
expiresAt: Date.now() + 3600000, // Cache for 1 hour
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
return count
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.error("Failed to fetch contributors:", error)
|
|
202
|
+
return 0
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Fetch repository star history (limited without token)
|
|
207
|
+
export async function fetchStarHistory(
|
|
208
|
+
owner: string,
|
|
209
|
+
repo: string,
|
|
210
|
+
token?: string
|
|
211
|
+
): Promise<StarHistory[]> {
|
|
212
|
+
const cacheKey = `star-history-${owner}-${repo}`
|
|
213
|
+
const cached = cache.get(cacheKey)
|
|
214
|
+
|
|
215
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
216
|
+
return cached.data
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
// This is a simplified version - for full star history you'd need
|
|
221
|
+
// to use the stargazers API with Accept: application/vnd.github.v3.star+json
|
|
222
|
+
// header and paginate through all results
|
|
223
|
+
const response = await githubFetch(`${API_BASE}/repos/${owner}/${repo}`, token)
|
|
224
|
+
const repoData = await response.json()
|
|
225
|
+
|
|
226
|
+
// For now, return current count as single data point
|
|
227
|
+
const history: StarHistory[] = [{
|
|
228
|
+
date: new Date().toISOString(),
|
|
229
|
+
count: repoData.stargazers_count,
|
|
230
|
+
repository: repoData.full_name,
|
|
231
|
+
}]
|
|
232
|
+
|
|
233
|
+
cache.set(cacheKey, {
|
|
234
|
+
data: history,
|
|
235
|
+
timestamp: Date.now(),
|
|
236
|
+
expiresAt: Date.now() + 3600000, // Cache for 1 hour
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
return history
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.error("Failed to fetch star history:", error)
|
|
242
|
+
return []
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Calculate repository statistics
|
|
247
|
+
export function calculateStats(repositories: GitHubRepository[]): GitHubStats {
|
|
248
|
+
const totalStars = repositories.reduce((sum, repo) => sum + repo.stargazers_count, 0)
|
|
249
|
+
const totalForks = repositories.reduce((sum, repo) => sum + repo.forks_count, 0)
|
|
250
|
+
const totalWatchers = repositories.reduce((sum, repo) => sum + repo.watchers_count, 0)
|
|
251
|
+
const totalIssues = repositories.reduce((sum, repo) => sum + repo.open_issues_count, 0)
|
|
252
|
+
|
|
253
|
+
const avgStarsPerRepo = repositories.length > 0 ? totalStars / repositories.length : 0
|
|
254
|
+
|
|
255
|
+
const mostStarredRepo = repositories.reduce((max, repo) =>
|
|
256
|
+
repo.stargazers_count > (max?.stargazers_count || 0) ? repo : max
|
|
257
|
+
, null as GitHubRepository | null)
|
|
258
|
+
|
|
259
|
+
// Calculate language statistics
|
|
260
|
+
const languageMap = new Map<string, number>()
|
|
261
|
+
repositories.forEach(repo => {
|
|
262
|
+
if (repo.language) {
|
|
263
|
+
languageMap.set(repo.language, (languageMap.get(repo.language) || 0) + 1)
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
const totalRepos = repositories.length
|
|
268
|
+
const languages: LanguageStats[] = Array.from(languageMap.entries())
|
|
269
|
+
.map(([language, count]) => ({
|
|
270
|
+
language,
|
|
271
|
+
count,
|
|
272
|
+
percentage: (count / totalRepos) * 100,
|
|
273
|
+
color: LANGUAGE_COLORS[language] || "#6b7280",
|
|
274
|
+
}))
|
|
275
|
+
.sort((a, b) => b.count - a.count)
|
|
276
|
+
|
|
277
|
+
// Generate recent activity (mock for now)
|
|
278
|
+
const recentActivity: GitHubActivity[] = repositories
|
|
279
|
+
.slice(0, 5)
|
|
280
|
+
.map(repo => ({
|
|
281
|
+
type: "star" as const,
|
|
282
|
+
repository: repo.full_name,
|
|
283
|
+
timestamp: repo.updated_at,
|
|
284
|
+
description: `Repository updated`,
|
|
285
|
+
}))
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
totalStars,
|
|
289
|
+
totalForks,
|
|
290
|
+
totalWatchers,
|
|
291
|
+
totalIssues,
|
|
292
|
+
avgStarsPerRepo,
|
|
293
|
+
mostStarredRepo,
|
|
294
|
+
recentActivity,
|
|
295
|
+
languages,
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Format numbers for display
|
|
300
|
+
export function formatNumber(num: number): string {
|
|
301
|
+
if (num >= 1000000) {
|
|
302
|
+
return (num / 1000000).toFixed(1) + "M"
|
|
303
|
+
}
|
|
304
|
+
if (num >= 1000) {
|
|
305
|
+
return (num / 1000).toFixed(1) + "k"
|
|
306
|
+
}
|
|
307
|
+
return num.toString()
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Format date for display
|
|
311
|
+
export function formatDate(dateString: string): string {
|
|
312
|
+
const date = new Date(dateString)
|
|
313
|
+
const now = new Date()
|
|
314
|
+
const diffMs = now.getTime() - date.getTime()
|
|
315
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
|
316
|
+
|
|
317
|
+
if (diffDays === 0) {
|
|
318
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
|
|
319
|
+
if (diffHours === 0) {
|
|
320
|
+
const diffMinutes = Math.floor(diffMs / (1000 * 60))
|
|
321
|
+
return `${diffMinutes} minutes ago`
|
|
322
|
+
}
|
|
323
|
+
return `${diffHours} hours ago`
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (diffDays === 1) return "yesterday"
|
|
327
|
+
if (diffDays < 7) return `${diffDays} days ago`
|
|
328
|
+
if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`
|
|
329
|
+
if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`
|
|
330
|
+
|
|
331
|
+
return date.toLocaleDateString("en-US", {
|
|
332
|
+
year: "numeric",
|
|
333
|
+
month: "short",
|
|
334
|
+
day: "numeric",
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Clear cache
|
|
339
|
+
export function clearCache(pattern?: string): void {
|
|
340
|
+
if (pattern) {
|
|
341
|
+
for (const key of cache.keys()) {
|
|
342
|
+
if (key.includes(pattern)) {
|
|
343
|
+
cache.delete(key)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
cache.clear()
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Export data as JSON
|
|
352
|
+
export function exportData(data: any, filename: string): void {
|
|
353
|
+
const jsonString = JSON.stringify(data, null, 2)
|
|
354
|
+
const blob = new Blob([jsonString], { type: "application/json" })
|
|
355
|
+
const url = URL.createObjectURL(blob)
|
|
356
|
+
|
|
357
|
+
const link = document.createElement("a")
|
|
358
|
+
link.href = url
|
|
359
|
+
link.download = filename
|
|
360
|
+
document.body.appendChild(link)
|
|
361
|
+
link.click()
|
|
362
|
+
document.body.removeChild(link)
|
|
363
|
+
|
|
364
|
+
URL.revokeObjectURL(url)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Export data as CSV
|
|
368
|
+
export function exportAsCSV(repositories: GitHubRepository[], filename: string): void {
|
|
369
|
+
const headers = [
|
|
370
|
+
"Name",
|
|
371
|
+
"Owner",
|
|
372
|
+
"Stars",
|
|
373
|
+
"Forks",
|
|
374
|
+
"Watchers",
|
|
375
|
+
"Issues",
|
|
376
|
+
"Language",
|
|
377
|
+
"Description",
|
|
378
|
+
"URL",
|
|
379
|
+
"Created",
|
|
380
|
+
"Updated",
|
|
381
|
+
]
|
|
382
|
+
|
|
383
|
+
const rows = repositories.map(repo => [
|
|
384
|
+
repo.name,
|
|
385
|
+
repo.owner.login,
|
|
386
|
+
repo.stargazers_count,
|
|
387
|
+
repo.forks_count,
|
|
388
|
+
repo.watchers_count,
|
|
389
|
+
repo.open_issues_count,
|
|
390
|
+
repo.language || "",
|
|
391
|
+
repo.description || "",
|
|
392
|
+
repo.html_url,
|
|
393
|
+
new Date(repo.created_at).toLocaleDateString(),
|
|
394
|
+
new Date(repo.updated_at).toLocaleDateString(),
|
|
395
|
+
])
|
|
396
|
+
|
|
397
|
+
const csvContent = [
|
|
398
|
+
headers.join(","),
|
|
399
|
+
...rows.map(row => row.map(cell => `"${cell}"`).join(",")),
|
|
400
|
+
].join("\n")
|
|
401
|
+
|
|
402
|
+
const blob = new Blob([csvContent], { type: "text/csv" })
|
|
403
|
+
const url = URL.createObjectURL(blob)
|
|
404
|
+
|
|
405
|
+
const link = document.createElement("a")
|
|
406
|
+
link.href = url
|
|
407
|
+
link.download = filename
|
|
408
|
+
document.body.appendChild(link)
|
|
409
|
+
link.click()
|
|
410
|
+
document.body.removeChild(link)
|
|
411
|
+
|
|
412
|
+
URL.revokeObjectURL(url)
|
|
413
|
+
}
|