@wakastellar/ui 2.0.0 → 2.1.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.
Files changed (291) hide show
  1. package/README.md +71 -8
  2. package/dist/cli/commands/add.d.ts +7 -0
  3. package/dist/cli/commands/init.d.ts +6 -0
  4. package/dist/cli/commands/list.d.ts +5 -0
  5. package/dist/cli/commands/search.d.ts +1 -0
  6. package/dist/cli/index.cjs +6014 -0
  7. package/dist/cli/index.d.ts +1 -0
  8. package/dist/cli/utils/config.d.ts +29 -0
  9. package/dist/cli/utils/logger.d.ts +20 -0
  10. package/dist/cli/utils/registry.d.ts +23 -0
  11. package/package.json +14 -3
  12. package/src/blocks/activity-timeline/index.tsx +586 -0
  13. package/src/blocks/calendar-view/index.tsx +756 -0
  14. package/src/blocks/chat/index.tsx +1018 -0
  15. package/src/blocks/chat/widget.tsx +504 -0
  16. package/src/blocks/dashboard/index.tsx +522 -0
  17. package/src/blocks/empty-states/index.tsx +452 -0
  18. package/src/blocks/error-pages/index.tsx +426 -0
  19. package/src/blocks/faq/index.tsx +479 -0
  20. package/src/blocks/file-manager/index.tsx +890 -0
  21. package/src/blocks/footer/index.tsx +133 -0
  22. package/src/blocks/header/index.tsx +357 -0
  23. package/src/blocks/headtab/index.tsx +139 -0
  24. package/src/blocks/i18n-editor/index.tsx +1016 -0
  25. package/src/blocks/index.ts +80 -0
  26. package/src/blocks/kanban-board/index.tsx +779 -0
  27. package/src/blocks/landing/index.tsx +677 -0
  28. package/src/blocks/language-selector/index.tsx +88 -0
  29. package/src/blocks/layout/index.tsx +159 -0
  30. package/src/blocks/login/index.tsx +339 -0
  31. package/src/blocks/login/types.ts +131 -0
  32. package/src/blocks/pricing/index.tsx +564 -0
  33. package/src/blocks/profile/index.tsx +746 -0
  34. package/src/blocks/settings/index.tsx +558 -0
  35. package/src/blocks/sidebar/index.tsx +713 -0
  36. package/src/blocks/theme-creator-block/index.tsx +835 -0
  37. package/src/blocks/user-management/index.tsx +1037 -0
  38. package/src/blocks/wizard/index.tsx +719 -0
  39. package/src/components/DataTable/DataTable.tsx +406 -0
  40. package/src/components/DataTable/DataTableAdvanced.tsx +720 -0
  41. package/src/components/DataTable/DataTableBody.tsx +216 -0
  42. package/src/components/DataTable/DataTableCell.tsx +172 -0
  43. package/src/components/DataTable/DataTableColumnResizer.tsx +62 -0
  44. package/src/components/DataTable/DataTableConflictResolver.tsx +478 -0
  45. package/src/components/DataTable/DataTableContextMenu.tsx +219 -0
  46. package/src/components/DataTable/DataTableEditCell.tsx +279 -0
  47. package/src/components/DataTable/DataTableFilterBuilder.tsx +519 -0
  48. package/src/components/DataTable/DataTableFilters.tsx +535 -0
  49. package/src/components/DataTable/DataTableGrouping.tsx +147 -0
  50. package/src/components/DataTable/DataTableHeader.tsx +172 -0
  51. package/src/components/DataTable/DataTablePagination.tsx +125 -0
  52. package/src/components/DataTable/DataTableSelection.tsx +269 -0
  53. package/src/components/DataTable/DataTableSyncStatus.tsx +281 -0
  54. package/src/components/DataTable/DataTableToolbar.tsx +262 -0
  55. package/src/components/DataTable/README.md +446 -0
  56. package/src/components/DataTable/__tests__/DataTableAdvanced.test.tsx +426 -0
  57. package/src/components/DataTable/__tests__/DataTableEdit.test.tsx +329 -0
  58. package/src/components/DataTable/__tests__/useDataTableAdvanced.test.ts +455 -0
  59. package/src/components/DataTable/examples/EditExample.tsx +166 -0
  60. package/src/components/DataTable/formatters/index.ts +335 -0
  61. package/src/components/DataTable/hooks/__tests__/useDataTableEdit.test.ts +239 -0
  62. package/src/components/DataTable/hooks/useDataTable.ts +145 -0
  63. package/src/components/DataTable/hooks/useDataTableAdvanced.ts +342 -0
  64. package/src/components/DataTable/hooks/useDataTableAdvancedFilters.ts +637 -0
  65. package/src/components/DataTable/hooks/useDataTableColumnTemplates.ts +186 -0
  66. package/src/components/DataTable/hooks/useDataTableEdit.ts +167 -0
  67. package/src/components/DataTable/hooks/useDataTableExport.ts +227 -0
  68. package/src/components/DataTable/hooks/useDataTableImport.ts +216 -0
  69. package/src/components/DataTable/hooks/useDataTableOffline.ts +481 -0
  70. package/src/components/DataTable/hooks/useDataTableTheme.ts +213 -0
  71. package/src/components/DataTable/hooks/useDataTableVirtualization.ts +99 -0
  72. package/src/components/DataTable/hooks/useTableLayout.ts +85 -0
  73. package/src/components/DataTable/index.ts +81 -0
  74. package/src/components/DataTable/services/IndexedDBService.ts +504 -0
  75. package/src/components/DataTable/templates/index.tsx +803 -0
  76. package/src/components/DataTable/types.ts +504 -0
  77. package/src/components/DataTable/utils.ts +164 -0
  78. package/src/components/DataTable/workers/exportWorker.ts +213 -0
  79. package/src/components/accordion/index.tsx +61 -0
  80. package/src/components/alert/index.tsx +61 -0
  81. package/src/components/alert-dialog/index.tsx +146 -0
  82. package/src/components/aspect-ratio/index.tsx +12 -0
  83. package/src/components/avatar/index.tsx +54 -0
  84. package/src/components/badge/Badge.stories.tsx +64 -0
  85. package/src/components/badge/index.tsx +38 -0
  86. package/src/components/button/Button.stories.tsx +173 -0
  87. package/src/components/button/index.tsx +56 -0
  88. package/src/components/calendar/index.tsx +73 -0
  89. package/src/components/card/index.tsx +78 -0
  90. package/src/components/checkbox/index.tsx +34 -0
  91. package/src/components/code/index.tsx +229 -0
  92. package/src/components/collapsible/index.tsx +16 -0
  93. package/src/components/command/index.tsx +162 -0
  94. package/src/components/context-menu/index.tsx +204 -0
  95. package/src/components/dialog/index.tsx +126 -0
  96. package/src/components/dropdown-menu/index.tsx +204 -0
  97. package/src/components/error-boundary/ErrorBoundary.tsx +281 -0
  98. package/src/components/error-boundary/index.ts +7 -0
  99. package/src/components/form/index.tsx +183 -0
  100. package/src/components/hover-card/index.tsx +33 -0
  101. package/src/components/index.ts +368 -0
  102. package/src/components/input/Input.stories.tsx +100 -0
  103. package/src/components/input/index.tsx +27 -0
  104. package/src/components/input-otp/index.tsx +277 -0
  105. package/src/components/label/index.tsx +30 -0
  106. package/src/components/language-selector/index.tsx +341 -0
  107. package/src/components/menubar/index.tsx +240 -0
  108. package/src/components/navigation-menu/index.tsx +134 -0
  109. package/src/components/popover/index.tsx +35 -0
  110. package/src/components/progress/index.tsx +32 -0
  111. package/src/components/radio-group/index.tsx +48 -0
  112. package/src/components/scroll-area/index.tsx +52 -0
  113. package/src/components/select/index.tsx +164 -0
  114. package/src/components/separator/index.tsx +35 -0
  115. package/src/components/sheet/index.tsx +147 -0
  116. package/src/components/skeleton/index.tsx +22 -0
  117. package/src/components/slider/index.tsx +32 -0
  118. package/src/components/switch/index.tsx +33 -0
  119. package/src/components/table/index.tsx +117 -0
  120. package/src/components/tabs/index.tsx +59 -0
  121. package/src/components/textarea/index.tsx +30 -0
  122. package/src/components/theme-selector/index.tsx +327 -0
  123. package/src/components/toast/index.tsx +133 -0
  124. package/src/components/toaster/index.tsx +34 -0
  125. package/src/components/toggle/index.tsx +49 -0
  126. package/src/components/tooltip/index.tsx +34 -0
  127. package/src/components/typography/index.tsx +276 -0
  128. package/src/components/waka-3d-pie-chart/index.tsx +486 -0
  129. package/src/components/waka-achievement-unlock/index.tsx +716 -0
  130. package/src/components/waka-activity-feed/index.tsx +686 -0
  131. package/src/components/waka-address-autocomplete/index.tsx +1202 -0
  132. package/src/components/waka-admincrumb/index.tsx +349 -0
  133. package/src/components/waka-alert-stack/index.tsx +827 -0
  134. package/src/components/waka-allocation-matrix/index.tsx +1278 -0
  135. package/src/components/waka-approval-chain/index.tsx +766 -0
  136. package/src/components/waka-audit-log/index.tsx +1475 -0
  137. package/src/components/waka-autocomplete/index.tsx +358 -0
  138. package/src/components/waka-badge-showcase/index.tsx +704 -0
  139. package/src/components/waka-barcode/index.tsx +260 -0
  140. package/src/components/waka-biometric-prompt/index.tsx +765 -0
  141. package/src/components/waka-bottom-sheet/index.tsx +495 -0
  142. package/src/components/waka-breadcrumb/index.tsx +376 -0
  143. package/src/components/waka-breadcrumb-path/index.tsx +513 -0
  144. package/src/components/waka-budget-burn/index.tsx +1234 -0
  145. package/src/components/waka-capacity-planner/index.tsx +1107 -0
  146. package/src/components/waka-carousel/index.tsx +893 -0
  147. package/src/components/waka-cart-summary/index.tsx +1055 -0
  148. package/src/components/waka-challenge-timer/index.tsx +1044 -0
  149. package/src/components/waka-charts/WakaAreaChart.tsx +251 -0
  150. package/src/components/waka-charts/WakaBarChart.tsx +222 -0
  151. package/src/components/waka-charts/WakaChart.tsx +124 -0
  152. package/src/components/waka-charts/WakaLineChart.tsx +219 -0
  153. package/src/components/waka-charts/WakaMiniChart.tsx +133 -0
  154. package/src/components/waka-charts/WakaPieChart.tsx +214 -0
  155. package/src/components/waka-charts/WakaSparkline.tsx +229 -0
  156. package/src/components/waka-charts/dataTableHelpers.ts +109 -0
  157. package/src/components/waka-charts/hooks/useChartTheme.ts +123 -0
  158. package/src/components/waka-charts/hooks/useRechartsLoader.ts +234 -0
  159. package/src/components/waka-charts/index.ts +90 -0
  160. package/src/components/waka-charts/types.ts +330 -0
  161. package/src/components/waka-chat-bubble/index.tsx +1060 -0
  162. package/src/components/waka-checklist/index.tsx +1067 -0
  163. package/src/components/waka-checkout-stepper/index.tsx +976 -0
  164. package/src/components/waka-cohort-table/index.tsx +1011 -0
  165. package/src/components/waka-color-picker/index.tsx +447 -0
  166. package/src/components/waka-combo-counter/index.tsx +864 -0
  167. package/src/components/waka-combobox/index.tsx +497 -0
  168. package/src/components/waka-command-bar/index.tsx +403 -0
  169. package/src/components/waka-compare-period/index.tsx +1230 -0
  170. package/src/components/waka-connection-matrix/index.tsx +1053 -0
  171. package/src/components/waka-contribution-graph/index.tsx +552 -0
  172. package/src/components/waka-cost-breakdown/index.tsx +1065 -0
  173. package/src/components/waka-coupon-input/index.tsx +592 -0
  174. package/src/components/waka-credit-card-input/index.tsx +982 -0
  175. package/src/components/waka-daily-reward/index.tsx +762 -0
  176. package/src/components/waka-date-range-picker/index.tsx +378 -0
  177. package/src/components/waka-datetime-picker/index.tsx +793 -0
  178. package/src/components/waka-datetime-picker.form-integration/index.tsx +402 -0
  179. package/src/components/waka-deployment-lane/index.tsx +673 -0
  180. package/src/components/waka-device-trust/index.tsx +1259 -0
  181. package/src/components/waka-dock/index.tsx +285 -0
  182. package/src/components/waka-drawer/index.tsx +319 -0
  183. package/src/components/waka-empty-state/index.tsx +545 -0
  184. package/src/components/waka-error-shake/index.tsx +398 -0
  185. package/src/components/waka-feature-announcement/index.tsx +991 -0
  186. package/src/components/waka-file-upload/index.tsx +437 -0
  187. package/src/components/waka-floating-nav/index.tsx +413 -0
  188. package/src/components/waka-flow-diagram/index.tsx +508 -0
  189. package/src/components/waka-funnel-chart/index.tsx +823 -0
  190. package/src/components/waka-glow-card/index.tsx +246 -0
  191. package/src/components/waka-goal-progress/index.tsx +1025 -0
  192. package/src/components/waka-haptic-button/index.tsx +388 -0
  193. package/src/components/waka-health-pulse/index.tsx +451 -0
  194. package/src/components/waka-heatmap/index.tsx +1026 -0
  195. package/src/components/waka-hotspot/index.tsx +682 -0
  196. package/src/components/waka-image/index.tsx +373 -0
  197. package/src/components/waka-incident-timeline/index.tsx +686 -0
  198. package/src/components/waka-invoice-preview/index.tsx +829 -0
  199. package/src/components/waka-kanban/index.tsx +646 -0
  200. package/src/components/waka-kpi-dashboard/index.tsx +755 -0
  201. package/src/components/waka-leaderboard/index.tsx +746 -0
  202. package/src/components/waka-level-progress/index.tsx +665 -0
  203. package/src/components/waka-liquid-button/index.tsx +520 -0
  204. package/src/components/waka-loading-orbit/index.tsx +478 -0
  205. package/src/components/waka-loot-box/index.tsx +1091 -0
  206. package/src/components/waka-magic-link/index.tsx +321 -0
  207. package/src/components/waka-magnetic-button/index.tsx +567 -0
  208. package/src/components/waka-mention-input/index.tsx +953 -0
  209. package/src/components/waka-metric-sparkline/index.tsx +627 -0
  210. package/src/components/waka-milestone-road/index.tsx +1064 -0
  211. package/src/components/waka-modal/index.tsx +374 -0
  212. package/src/components/waka-morph-button/index.tsx +495 -0
  213. package/src/components/waka-network-topology/index.tsx +801 -0
  214. package/src/components/waka-notifications/index.tsx +414 -0
  215. package/src/components/waka-number-input/index.tsx +373 -0
  216. package/src/components/waka-orbital-menu/index.tsx +445 -0
  217. package/src/components/waka-order-tracker/index.tsx +1041 -0
  218. package/src/components/waka-pagination/index.tsx +393 -0
  219. package/src/components/waka-password-strength/index.tsx +824 -0
  220. package/src/components/waka-payment-method-picker/index.tsx +715 -0
  221. package/src/components/waka-permission-matrix/index.tsx +1302 -0
  222. package/src/components/waka-phone-input/index.tsx +801 -0
  223. package/src/components/waka-pipeline-view/index.tsx +604 -0
  224. package/src/components/waka-player-card/index.tsx +691 -0
  225. package/src/components/waka-points-popup/index.tsx +366 -0
  226. package/src/components/waka-power-up/index.tsx +1155 -0
  227. package/src/components/waka-presence-indicator/index.tsx +1181 -0
  228. package/src/components/waka-pricing-table/index.tsx +755 -0
  229. package/src/components/waka-product-card/index.tsx +786 -0
  230. package/src/components/waka-progress-onboarding/index.tsx +878 -0
  231. package/src/components/waka-pull-to-refresh/index.tsx +451 -0
  232. package/src/components/waka-qrcode/index.tsx +232 -0
  233. package/src/components/waka-quest-card/index.tsx +1275 -0
  234. package/src/components/waka-quota-bar/index.tsx +693 -0
  235. package/src/components/waka-radar-score/index.tsx +512 -0
  236. package/src/components/waka-rank-badge/index.tsx +813 -0
  237. package/src/components/waka-rating-input/index.tsx +560 -0
  238. package/src/components/waka-reaction-picker/index.tsx +1062 -0
  239. package/src/components/waka-region-map/index.tsx +730 -0
  240. package/src/components/waka-resource-gauge/index.tsx +654 -0
  241. package/src/components/waka-resource-pool/index.tsx +1035 -0
  242. package/src/components/waka-rich-text-editor/index.tsx +594 -0
  243. package/src/components/waka-rollback-slider/index.tsx +891 -0
  244. package/src/components/waka-sankey-diagram/index.tsx +1032 -0
  245. package/src/components/waka-schedule-picker/index.tsx +1060 -0
  246. package/src/components/waka-scratch-card/index.tsx +914 -0
  247. package/src/components/waka-season-pass/index.tsx +886 -0
  248. package/src/components/waka-security-score/index.tsx +1126 -0
  249. package/src/components/waka-segmented-control/index.tsx +238 -0
  250. package/src/components/waka-server-rack/index.tsx +764 -0
  251. package/src/components/waka-session-manager/index.tsx +815 -0
  252. package/src/components/waka-signature-pad/index.tsx +744 -0
  253. package/src/components/waka-skeleton-wave/index.tsx +454 -0
  254. package/src/components/waka-skill-tree/index.tsx +1031 -0
  255. package/src/components/waka-sla-tracker/index.tsx +798 -0
  256. package/src/components/waka-slider-range/index.tsx +765 -0
  257. package/src/components/waka-spin-wheel/index.tsx +671 -0
  258. package/src/components/waka-spinner/index.tsx +284 -0
  259. package/src/components/waka-spotlight/index.tsx +410 -0
  260. package/src/components/waka-stat/index.tsx +428 -0
  261. package/src/components/waka-stats-hexagon/index.tsx +824 -0
  262. package/src/components/waka-status-matrix/index.tsx +565 -0
  263. package/src/components/waka-stepper/index.tsx +489 -0
  264. package/src/components/waka-streak-counter/index.tsx +334 -0
  265. package/src/components/waka-success-explosion/index.tsx +453 -0
  266. package/src/components/waka-swipe-card/index.tsx +574 -0
  267. package/src/components/waka-tabs-morph/index.tsx +509 -0
  268. package/src/components/waka-tag-input/index.tsx +877 -0
  269. package/src/components/waka-team-banner/index.tsx +1183 -0
  270. package/src/components/waka-terminal-output/index.tsx +836 -0
  271. package/src/components/waka-theme-creator/index.tsx +762 -0
  272. package/src/components/waka-theme-manager/index.tsx +654 -0
  273. package/src/components/waka-thread-view/index.tsx +874 -0
  274. package/src/components/waka-tilt-card/index.tsx +250 -0
  275. package/src/components/waka-time-picker/index.tsx +479 -0
  276. package/src/components/waka-timeline/index.tsx +385 -0
  277. package/src/components/waka-tooltip-tour/index.tsx +855 -0
  278. package/src/components/waka-tour-guide/index.tsx +920 -0
  279. package/src/components/waka-tournament-bracket/index.tsx +1276 -0
  280. package/src/components/waka-tree/index.tsx +557 -0
  281. package/src/components/waka-treemap-chart/index.tsx +1031 -0
  282. package/src/components/waka-two-factor-setup/index.tsx +995 -0
  283. package/src/components/waka-typewriter/index.tsx +566 -0
  284. package/src/components/waka-typing-indicator/index.tsx +649 -0
  285. package/src/components/waka-versus-card/index.tsx +1026 -0
  286. package/src/components/waka-video/index.tsx +557 -0
  287. package/src/components/waka-video-call/index.tsx +1087 -0
  288. package/src/components/waka-virtual-list/index.tsx +327 -0
  289. package/src/components/waka-voice-message/index.tsx +1019 -0
  290. package/src/components/waka-welcome-modal/index.tsx +790 -0
  291. package/src/components/waka-xp-bar/index.tsx +799 -0
@@ -0,0 +1,686 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils/cn"
5
+ import {
6
+ Trophy,
7
+ TrendingUp,
8
+ ShoppingCart,
9
+ UserPlus,
10
+ Swords,
11
+ Flag,
12
+ Heart,
13
+ MessageCircle,
14
+ Loader2,
15
+ ChevronDown,
16
+ } from "lucide-react"
17
+ import { Button } from "../button"
18
+
19
+ // ============================================================================
20
+ // Types
21
+ // ============================================================================
22
+
23
+ export type ActivityType =
24
+ | "achievement"
25
+ | "level_up"
26
+ | "purchase"
27
+ | "follow"
28
+ | "challenge"
29
+ | "milestone"
30
+
31
+ export interface ActivityUser {
32
+ /** Unique user identifier */
33
+ id: string
34
+ /** Display name */
35
+ name: string
36
+ /** Avatar URL */
37
+ avatar?: string
38
+ }
39
+
40
+ export interface Activity {
41
+ /** Unique activity identifier */
42
+ id: string
43
+ /** Activity type */
44
+ type: ActivityType
45
+ /** User who performed the activity */
46
+ user: ActivityUser
47
+ /** Activity content/description */
48
+ content: string
49
+ /** Additional metadata */
50
+ metadata?: Record<string, any>
51
+ /** When the activity occurred */
52
+ timestamp: Date
53
+ /** Number of likes */
54
+ likes?: number
55
+ /** Whether current user liked this activity */
56
+ liked?: boolean
57
+ }
58
+
59
+ export interface GroupedActivity extends Omit<Activity, "user"> {
60
+ /** Primary user */
61
+ user: ActivityUser
62
+ /** Additional users in the group */
63
+ groupedUsers: ActivityUser[]
64
+ /** Total count of users */
65
+ totalUsers: number
66
+ }
67
+
68
+ export interface WakaActivityFeedProps {
69
+ /** List of activities to display */
70
+ activities: Activity[]
71
+ /** Callback when like button is clicked */
72
+ onLike?: (activityId: string) => void
73
+ /** Callback when load more is triggered */
74
+ onLoadMore?: () => void
75
+ /** Whether more activities can be loaded */
76
+ hasMore?: boolean
77
+ /** Whether activities are currently loading */
78
+ loading?: boolean
79
+ /** Show like button and count */
80
+ showLikes?: boolean
81
+ /** Group similar activities from multiple users */
82
+ groupSimilar?: boolean
83
+ /** Maximum number of items to display */
84
+ maxItems?: number
85
+ /** Enable entry animations */
86
+ animated?: boolean
87
+ /** Custom className */
88
+ className?: string
89
+ }
90
+
91
+ // ============================================================================
92
+ // Activity Type Configuration
93
+ // ============================================================================
94
+
95
+ const activityConfig: Record<
96
+ ActivityType,
97
+ { icon: React.ElementType; color: string; bgColor: string; label: string }
98
+ > = {
99
+ achievement: {
100
+ icon: Trophy,
101
+ color: "text-yellow-600",
102
+ bgColor: "bg-yellow-100",
103
+ label: "Achievement",
104
+ },
105
+ level_up: {
106
+ icon: TrendingUp,
107
+ color: "text-green-600",
108
+ bgColor: "bg-green-100",
109
+ label: "Level Up",
110
+ },
111
+ purchase: {
112
+ icon: ShoppingCart,
113
+ color: "text-blue-600",
114
+ bgColor: "bg-blue-100",
115
+ label: "Purchase",
116
+ },
117
+ follow: {
118
+ icon: UserPlus,
119
+ color: "text-purple-600",
120
+ bgColor: "bg-purple-100",
121
+ label: "Follow",
122
+ },
123
+ challenge: {
124
+ icon: Swords,
125
+ color: "text-red-600",
126
+ bgColor: "bg-red-100",
127
+ label: "Challenge",
128
+ },
129
+ milestone: {
130
+ icon: Flag,
131
+ color: "text-orange-600",
132
+ bgColor: "bg-orange-100",
133
+ label: "Milestone",
134
+ },
135
+ }
136
+
137
+ // ============================================================================
138
+ // Helper Functions
139
+ // ============================================================================
140
+
141
+ function formatRelativeTime(date: Date): string {
142
+ const now = new Date()
143
+ const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000)
144
+
145
+ if (diffInSeconds < 60) {
146
+ return "just now"
147
+ }
148
+
149
+ const diffInMinutes = Math.floor(diffInSeconds / 60)
150
+ if (diffInMinutes < 60) {
151
+ return `${diffInMinutes}m ago`
152
+ }
153
+
154
+ const diffInHours = Math.floor(diffInMinutes / 60)
155
+ if (diffInHours < 24) {
156
+ return `${diffInHours}h ago`
157
+ }
158
+
159
+ const diffInDays = Math.floor(diffInHours / 24)
160
+ if (diffInDays < 7) {
161
+ return `${diffInDays}d ago`
162
+ }
163
+
164
+ const diffInWeeks = Math.floor(diffInDays / 7)
165
+ if (diffInWeeks < 4) {
166
+ return `${diffInWeeks}w ago`
167
+ }
168
+
169
+ const diffInMonths = Math.floor(diffInDays / 30)
170
+ if (diffInMonths < 12) {
171
+ return `${diffInMonths}mo ago`
172
+ }
173
+
174
+ const diffInYears = Math.floor(diffInDays / 365)
175
+ return `${diffInYears}y ago`
176
+ }
177
+
178
+ function groupActivities(activities: Activity[]): (Activity | GroupedActivity)[] {
179
+ const grouped: (Activity | GroupedActivity)[] = []
180
+ const processed = new Set<string>()
181
+
182
+ for (const activity of activities) {
183
+ if (processed.has(activity.id)) continue
184
+
185
+ // Find similar activities (same type and content within a time window)
186
+ const similar = activities.filter((a) => {
187
+ if (a.id === activity.id || processed.has(a.id)) return false
188
+ if (a.type !== activity.type) return false
189
+ if (a.content !== activity.content) return false
190
+
191
+ // Within 1 hour time window
192
+ const timeDiff = Math.abs(
193
+ new Date(a.timestamp).getTime() - new Date(activity.timestamp).getTime()
194
+ )
195
+ return timeDiff < 60 * 60 * 1000
196
+ })
197
+
198
+ if (similar.length > 0) {
199
+ // Create grouped activity
200
+ const groupedUsers = similar.map((a) => a.user)
201
+ const allUsers = [activity.user, ...groupedUsers]
202
+
203
+ grouped.push({
204
+ ...activity,
205
+ user: activity.user,
206
+ groupedUsers,
207
+ totalUsers: allUsers.length,
208
+ } as GroupedActivity)
209
+
210
+ // Mark all as processed
211
+ similar.forEach((a) => processed.add(a.id))
212
+ } else {
213
+ grouped.push(activity)
214
+ }
215
+
216
+ processed.add(activity.id)
217
+ }
218
+
219
+ return grouped
220
+ }
221
+
222
+ function isGroupedActivity(
223
+ activity: Activity | GroupedActivity
224
+ ): activity is GroupedActivity {
225
+ return "groupedUsers" in activity && "totalUsers" in activity
226
+ }
227
+
228
+ // ============================================================================
229
+ // Avatar Component
230
+ // ============================================================================
231
+
232
+ interface AvatarProps {
233
+ user: ActivityUser
234
+ size?: "sm" | "md" | "lg"
235
+ className?: string
236
+ }
237
+
238
+ function Avatar({ user, size = "md", className }: AvatarProps) {
239
+ const sizeClasses = {
240
+ sm: "h-6 w-6 text-[10px]",
241
+ md: "h-10 w-10 text-sm",
242
+ lg: "h-12 w-12 text-base",
243
+ }
244
+
245
+ const getInitials = (name: string) => {
246
+ return name
247
+ .split(" ")
248
+ .map((n) => n[0])
249
+ .join("")
250
+ .toUpperCase()
251
+ .slice(0, 2)
252
+ }
253
+
254
+ return (
255
+ <div
256
+ className={cn(
257
+ "relative flex items-center justify-center rounded-full font-semibold flex-shrink-0",
258
+ sizeClasses[size],
259
+ user.avatar ? "" : "bg-muted text-muted-foreground",
260
+ className
261
+ )}
262
+ >
263
+ {user.avatar ? (
264
+ <img
265
+ src={user.avatar}
266
+ alt={user.name}
267
+ className="h-full w-full rounded-full object-cover"
268
+ />
269
+ ) : (
270
+ getInitials(user.name)
271
+ )}
272
+ </div>
273
+ )
274
+ }
275
+
276
+ // ============================================================================
277
+ // Stacked Avatars Component
278
+ // ============================================================================
279
+
280
+ interface StackedAvatarsProps {
281
+ users: ActivityUser[]
282
+ max?: number
283
+ size?: "sm" | "md"
284
+ }
285
+
286
+ function StackedAvatars({ users, max = 3, size = "sm" }: StackedAvatarsProps) {
287
+ const displayUsers = users.slice(0, max)
288
+ const remaining = users.length - max
289
+
290
+ return (
291
+ <div className="flex -space-x-2">
292
+ {displayUsers.map((user, index) => (
293
+ <Avatar
294
+ key={user.id}
295
+ user={user}
296
+ size={size}
297
+ className={cn(
298
+ "ring-2 ring-background",
299
+ index > 0 && "relative"
300
+ )}
301
+ />
302
+ ))}
303
+ {remaining > 0 && (
304
+ <div
305
+ className={cn(
306
+ "flex items-center justify-center rounded-full bg-muted text-muted-foreground font-medium ring-2 ring-background",
307
+ size === "sm" ? "h-6 w-6 text-[10px]" : "h-8 w-8 text-xs"
308
+ )}
309
+ >
310
+ +{remaining}
311
+ </div>
312
+ )}
313
+ </div>
314
+ )
315
+ }
316
+
317
+ // ============================================================================
318
+ // Activity Icon Component
319
+ // ============================================================================
320
+
321
+ interface ActivityIconProps {
322
+ type: ActivityType
323
+ className?: string
324
+ }
325
+
326
+ function ActivityIcon({ type, className }: ActivityIconProps) {
327
+ const config = activityConfig[type]
328
+ const Icon = config.icon
329
+
330
+ return (
331
+ <div
332
+ className={cn(
333
+ "flex h-8 w-8 items-center justify-center rounded-full flex-shrink-0",
334
+ config.bgColor,
335
+ className
336
+ )}
337
+ >
338
+ <Icon className={cn("h-4 w-4", config.color)} />
339
+ </div>
340
+ )
341
+ }
342
+
343
+ // ============================================================================
344
+ // Activity Item Component
345
+ // ============================================================================
346
+
347
+ interface ActivityItemProps {
348
+ activity: Activity | GroupedActivity
349
+ showLikes: boolean
350
+ onLike?: (activityId: string) => void
351
+ animated: boolean
352
+ isNew?: boolean
353
+ }
354
+
355
+ function ActivityItem({
356
+ activity,
357
+ showLikes,
358
+ onLike,
359
+ animated,
360
+ isNew,
361
+ }: ActivityItemProps) {
362
+ const [isAnimating, setIsAnimating] = React.useState(isNew && animated)
363
+ const isGrouped = isGroupedActivity(activity)
364
+
365
+ React.useEffect(() => {
366
+ if (isNew && animated) {
367
+ setIsAnimating(true)
368
+ const timer = setTimeout(() => setIsAnimating(false), 500)
369
+ return () => clearTimeout(timer)
370
+ }
371
+ }, [isNew, animated])
372
+
373
+ const handleLike = (e: React.MouseEvent) => {
374
+ e.stopPropagation()
375
+ onLike?.(activity.id)
376
+ }
377
+
378
+ const renderUserInfo = () => {
379
+ if (isGrouped) {
380
+ const otherCount = activity.totalUsers - 1
381
+ return (
382
+ <span className="font-medium">
383
+ {activity.user.name}
384
+ {otherCount > 0 && (
385
+ <span className="text-muted-foreground font-normal">
386
+ {" "}
387
+ and {otherCount} {otherCount === 1 ? "other" : "others"}
388
+ </span>
389
+ )}
390
+ </span>
391
+ )
392
+ }
393
+ return <span className="font-medium">{activity.user.name}</span>
394
+ }
395
+
396
+ return (
397
+ <div
398
+ className={cn(
399
+ "group flex gap-3 rounded-lg border bg-card p-4 transition-all duration-300",
400
+ "hover:bg-accent/50 hover:shadow-sm",
401
+ isAnimating && "animate-slide-in-fade"
402
+ )}
403
+ >
404
+ {/* Avatar Section */}
405
+ <div className="flex-shrink-0">
406
+ {isGrouped && activity.groupedUsers.length > 0 ? (
407
+ <StackedAvatars
408
+ users={[activity.user, ...activity.groupedUsers]}
409
+ max={3}
410
+ size="sm"
411
+ />
412
+ ) : (
413
+ <Avatar user={activity.user} size="md" />
414
+ )}
415
+ </div>
416
+
417
+ {/* Content Section */}
418
+ <div className="flex-1 min-w-0">
419
+ <div className="flex items-start justify-between gap-2">
420
+ <div className="flex-1 min-w-0">
421
+ <div className="flex items-center gap-2 flex-wrap">
422
+ {renderUserInfo()}
423
+ <ActivityIcon type={activity.type} className="h-5 w-5" />
424
+ </div>
425
+ <p className="mt-1 text-sm text-foreground">{activity.content}</p>
426
+ </div>
427
+
428
+ {/* Timestamp */}
429
+ <span className="flex-shrink-0 text-xs text-muted-foreground">
430
+ {formatRelativeTime(new Date(activity.timestamp))}
431
+ </span>
432
+ </div>
433
+
434
+ {/* Metadata */}
435
+ {activity.metadata && Object.keys(activity.metadata).length > 0 && (
436
+ <div className="mt-2 flex flex-wrap gap-2">
437
+ {Object.entries(activity.metadata).map(([key, value]) => (
438
+ <span
439
+ key={key}
440
+ className="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground"
441
+ >
442
+ {key}: {String(value)}
443
+ </span>
444
+ ))}
445
+ </div>
446
+ )}
447
+
448
+ {/* Actions */}
449
+ {showLikes && (
450
+ <div className="mt-3 flex items-center gap-4">
451
+ <button
452
+ onClick={handleLike}
453
+ className={cn(
454
+ "flex items-center gap-1.5 text-sm transition-colors",
455
+ activity.liked
456
+ ? "text-red-500"
457
+ : "text-muted-foreground hover:text-red-500"
458
+ )}
459
+ >
460
+ <Heart
461
+ className={cn("h-4 w-4", activity.liked && "fill-current")}
462
+ />
463
+ <span>{activity.likes || 0}</span>
464
+ </button>
465
+ </div>
466
+ )}
467
+ </div>
468
+
469
+ <style>{`
470
+ @keyframes slide-in-fade {
471
+ 0% {
472
+ opacity: 0;
473
+ transform: translateY(-10px);
474
+ }
475
+ 100% {
476
+ opacity: 1;
477
+ transform: translateY(0);
478
+ }
479
+ }
480
+ .animate-slide-in-fade {
481
+ animation: slide-in-fade 0.3s ease-out;
482
+ }
483
+ `}</style>
484
+ </div>
485
+ )
486
+ }
487
+
488
+ // ============================================================================
489
+ // Main Component
490
+ // ============================================================================
491
+
492
+ export function WakaActivityFeed({
493
+ activities,
494
+ onLike,
495
+ onLoadMore,
496
+ hasMore = false,
497
+ loading = false,
498
+ showLikes = true,
499
+ groupSimilar = false,
500
+ maxItems,
501
+ animated = true,
502
+ className,
503
+ }: WakaActivityFeedProps) {
504
+ const [newActivityIds, setNewActivityIds] = React.useState<Set<string>>(new Set())
505
+ const prevActivityIdsRef = React.useRef<Set<string>>(new Set())
506
+
507
+ // Track new activities for animation
508
+ React.useEffect(() => {
509
+ const currentIds = new Set(activities.map((a) => a.id))
510
+ const newIds = new Set<string>()
511
+
512
+ currentIds.forEach((id) => {
513
+ if (!prevActivityIdsRef.current.has(id)) {
514
+ newIds.add(id)
515
+ }
516
+ })
517
+
518
+ if (newIds.size > 0 && prevActivityIdsRef.current.size > 0) {
519
+ setNewActivityIds(newIds)
520
+ const timer = setTimeout(() => setNewActivityIds(new Set()), 500)
521
+ return () => clearTimeout(timer)
522
+ }
523
+
524
+ prevActivityIdsRef.current = currentIds
525
+ }, [activities])
526
+
527
+ // Process activities
528
+ const processedActivities = React.useMemo(() => {
529
+ let result: (Activity | GroupedActivity)[] = groupSimilar
530
+ ? groupActivities(activities)
531
+ : activities
532
+
533
+ if (maxItems && maxItems > 0) {
534
+ result = result.slice(0, maxItems)
535
+ }
536
+
537
+ return result
538
+ }, [activities, groupSimilar, maxItems])
539
+
540
+ if (processedActivities.length === 0 && !loading) {
541
+ return (
542
+ <div className={cn("flex flex-col items-center justify-center py-12 text-center", className)}>
543
+ <MessageCircle className="h-12 w-12 text-muted-foreground/50 mb-4" />
544
+ <h3 className="text-lg font-medium text-muted-foreground">No activities yet</h3>
545
+ <p className="text-sm text-muted-foreground/70 mt-1">
546
+ When there's activity, you'll see it here.
547
+ </p>
548
+ </div>
549
+ )
550
+ }
551
+
552
+ return (
553
+ <div className={cn("space-y-3", className)}>
554
+ {/* Activity List */}
555
+ {processedActivities.map((activity) => (
556
+ <ActivityItem
557
+ key={activity.id}
558
+ activity={activity}
559
+ showLikes={showLikes}
560
+ onLike={onLike}
561
+ animated={animated}
562
+ isNew={newActivityIds.has(activity.id)}
563
+ />
564
+ ))}
565
+
566
+ {/* Loading State */}
567
+ {loading && (
568
+ <div className="flex items-center justify-center py-4">
569
+ <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
570
+ </div>
571
+ )}
572
+
573
+ {/* Load More */}
574
+ {!loading && hasMore && onLoadMore && (
575
+ <div className="flex justify-center pt-2">
576
+ <Button
577
+ variant="outline"
578
+ size="sm"
579
+ onClick={onLoadMore}
580
+ className="gap-2"
581
+ >
582
+ <ChevronDown className="h-4 w-4" />
583
+ Load more
584
+ </Button>
585
+ </div>
586
+ )}
587
+ </div>
588
+ )
589
+ }
590
+
591
+ // ============================================================================
592
+ // Hook for managing activity feed data
593
+ // ============================================================================
594
+
595
+ export interface UseActivityFeedOptions {
596
+ /** Initial activities */
597
+ initialActivities?: Activity[]
598
+ /** Maximum activities to keep in memory */
599
+ maxActivities?: number
600
+ }
601
+
602
+ export function useActivityFeed({
603
+ initialActivities = [],
604
+ maxActivities = 100,
605
+ }: UseActivityFeedOptions = {}) {
606
+ const [activities, setActivities] = React.useState<Activity[]>(initialActivities)
607
+ const [loading, setLoading] = React.useState(false)
608
+
609
+ const addActivity = React.useCallback(
610
+ (activity: Activity) => {
611
+ setActivities((prev) => {
612
+ const updated = [activity, ...prev]
613
+ return updated.slice(0, maxActivities)
614
+ })
615
+ },
616
+ [maxActivities]
617
+ )
618
+
619
+ const addActivities = React.useCallback(
620
+ (newActivities: Activity[]) => {
621
+ setActivities((prev) => {
622
+ const updated = [...newActivities, ...prev]
623
+ return updated.slice(0, maxActivities)
624
+ })
625
+ },
626
+ [maxActivities]
627
+ )
628
+
629
+ const removeActivity = React.useCallback((activityId: string) => {
630
+ setActivities((prev) => prev.filter((a) => a.id !== activityId))
631
+ }, [])
632
+
633
+ const toggleLike = React.useCallback((activityId: string) => {
634
+ setActivities((prev) =>
635
+ prev.map((a) =>
636
+ a.id === activityId
637
+ ? {
638
+ ...a,
639
+ liked: !a.liked,
640
+ likes: a.liked ? (a.likes || 1) - 1 : (a.likes || 0) + 1,
641
+ }
642
+ : a
643
+ )
644
+ )
645
+ }, [])
646
+
647
+ const loadMore = React.useCallback(
648
+ async (fetcher: () => Promise<Activity[]>) => {
649
+ setLoading(true)
650
+ try {
651
+ const moreActivities = await fetcher()
652
+ setActivities((prev) => {
653
+ const existingIds = new Set(prev.map((a) => a.id))
654
+ const newActivities = moreActivities.filter((a) => !existingIds.has(a.id))
655
+ return [...prev, ...newActivities].slice(0, maxActivities)
656
+ })
657
+ } finally {
658
+ setLoading(false)
659
+ }
660
+ },
661
+ [maxActivities]
662
+ )
663
+
664
+ const refresh = React.useCallback((newActivities: Activity[]) => {
665
+ setActivities(newActivities)
666
+ }, [])
667
+
668
+ const clear = React.useCallback(() => {
669
+ setActivities([])
670
+ }, [])
671
+
672
+ return {
673
+ activities,
674
+ loading,
675
+ addActivity,
676
+ addActivities,
677
+ removeActivity,
678
+ toggleLike,
679
+ loadMore,
680
+ refresh,
681
+ clear,
682
+ setLoading,
683
+ }
684
+ }
685
+
686
+ export default WakaActivityFeed