@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.
- package/README.md +71 -8
- package/dist/cli/commands/add.d.ts +7 -0
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/list.d.ts +5 -0
- package/dist/cli/commands/search.d.ts +1 -0
- package/dist/cli/index.cjs +6014 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/utils/config.d.ts +29 -0
- package/dist/cli/utils/logger.d.ts +20 -0
- package/dist/cli/utils/registry.d.ts +23 -0
- package/package.json +14 -3
- package/src/blocks/activity-timeline/index.tsx +586 -0
- package/src/blocks/calendar-view/index.tsx +756 -0
- package/src/blocks/chat/index.tsx +1018 -0
- package/src/blocks/chat/widget.tsx +504 -0
- package/src/blocks/dashboard/index.tsx +522 -0
- package/src/blocks/empty-states/index.tsx +452 -0
- package/src/blocks/error-pages/index.tsx +426 -0
- package/src/blocks/faq/index.tsx +479 -0
- package/src/blocks/file-manager/index.tsx +890 -0
- package/src/blocks/footer/index.tsx +133 -0
- package/src/blocks/header/index.tsx +357 -0
- package/src/blocks/headtab/index.tsx +139 -0
- package/src/blocks/i18n-editor/index.tsx +1016 -0
- package/src/blocks/index.ts +80 -0
- package/src/blocks/kanban-board/index.tsx +779 -0
- package/src/blocks/landing/index.tsx +677 -0
- package/src/blocks/language-selector/index.tsx +88 -0
- package/src/blocks/layout/index.tsx +159 -0
- package/src/blocks/login/index.tsx +339 -0
- package/src/blocks/login/types.ts +131 -0
- package/src/blocks/pricing/index.tsx +564 -0
- package/src/blocks/profile/index.tsx +746 -0
- package/src/blocks/settings/index.tsx +558 -0
- package/src/blocks/sidebar/index.tsx +713 -0
- package/src/blocks/theme-creator-block/index.tsx +835 -0
- package/src/blocks/user-management/index.tsx +1037 -0
- package/src/blocks/wizard/index.tsx +719 -0
- package/src/components/DataTable/DataTable.tsx +406 -0
- package/src/components/DataTable/DataTableAdvanced.tsx +720 -0
- package/src/components/DataTable/DataTableBody.tsx +216 -0
- package/src/components/DataTable/DataTableCell.tsx +172 -0
- package/src/components/DataTable/DataTableColumnResizer.tsx +62 -0
- package/src/components/DataTable/DataTableConflictResolver.tsx +478 -0
- package/src/components/DataTable/DataTableContextMenu.tsx +219 -0
- package/src/components/DataTable/DataTableEditCell.tsx +279 -0
- package/src/components/DataTable/DataTableFilterBuilder.tsx +519 -0
- package/src/components/DataTable/DataTableFilters.tsx +535 -0
- package/src/components/DataTable/DataTableGrouping.tsx +147 -0
- package/src/components/DataTable/DataTableHeader.tsx +172 -0
- package/src/components/DataTable/DataTablePagination.tsx +125 -0
- package/src/components/DataTable/DataTableSelection.tsx +269 -0
- package/src/components/DataTable/DataTableSyncStatus.tsx +281 -0
- package/src/components/DataTable/DataTableToolbar.tsx +262 -0
- package/src/components/DataTable/README.md +446 -0
- package/src/components/DataTable/__tests__/DataTableAdvanced.test.tsx +426 -0
- package/src/components/DataTable/__tests__/DataTableEdit.test.tsx +329 -0
- package/src/components/DataTable/__tests__/useDataTableAdvanced.test.ts +455 -0
- package/src/components/DataTable/examples/EditExample.tsx +166 -0
- package/src/components/DataTable/formatters/index.ts +335 -0
- package/src/components/DataTable/hooks/__tests__/useDataTableEdit.test.ts +239 -0
- package/src/components/DataTable/hooks/useDataTable.ts +145 -0
- package/src/components/DataTable/hooks/useDataTableAdvanced.ts +342 -0
- package/src/components/DataTable/hooks/useDataTableAdvancedFilters.ts +637 -0
- package/src/components/DataTable/hooks/useDataTableColumnTemplates.ts +186 -0
- package/src/components/DataTable/hooks/useDataTableEdit.ts +167 -0
- package/src/components/DataTable/hooks/useDataTableExport.ts +227 -0
- package/src/components/DataTable/hooks/useDataTableImport.ts +216 -0
- package/src/components/DataTable/hooks/useDataTableOffline.ts +481 -0
- package/src/components/DataTable/hooks/useDataTableTheme.ts +213 -0
- package/src/components/DataTable/hooks/useDataTableVirtualization.ts +99 -0
- package/src/components/DataTable/hooks/useTableLayout.ts +85 -0
- package/src/components/DataTable/index.ts +81 -0
- package/src/components/DataTable/services/IndexedDBService.ts +504 -0
- package/src/components/DataTable/templates/index.tsx +803 -0
- package/src/components/DataTable/types.ts +504 -0
- package/src/components/DataTable/utils.ts +164 -0
- package/src/components/DataTable/workers/exportWorker.ts +213 -0
- package/src/components/accordion/index.tsx +61 -0
- package/src/components/alert/index.tsx +61 -0
- package/src/components/alert-dialog/index.tsx +146 -0
- package/src/components/aspect-ratio/index.tsx +12 -0
- package/src/components/avatar/index.tsx +54 -0
- package/src/components/badge/Badge.stories.tsx +64 -0
- package/src/components/badge/index.tsx +38 -0
- package/src/components/button/Button.stories.tsx +173 -0
- package/src/components/button/index.tsx +56 -0
- package/src/components/calendar/index.tsx +73 -0
- package/src/components/card/index.tsx +78 -0
- package/src/components/checkbox/index.tsx +34 -0
- package/src/components/code/index.tsx +229 -0
- package/src/components/collapsible/index.tsx +16 -0
- package/src/components/command/index.tsx +162 -0
- package/src/components/context-menu/index.tsx +204 -0
- package/src/components/dialog/index.tsx +126 -0
- package/src/components/dropdown-menu/index.tsx +204 -0
- package/src/components/error-boundary/ErrorBoundary.tsx +281 -0
- package/src/components/error-boundary/index.ts +7 -0
- package/src/components/form/index.tsx +183 -0
- package/src/components/hover-card/index.tsx +33 -0
- package/src/components/index.ts +368 -0
- package/src/components/input/Input.stories.tsx +100 -0
- package/src/components/input/index.tsx +27 -0
- package/src/components/input-otp/index.tsx +277 -0
- package/src/components/label/index.tsx +30 -0
- package/src/components/language-selector/index.tsx +341 -0
- package/src/components/menubar/index.tsx +240 -0
- package/src/components/navigation-menu/index.tsx +134 -0
- package/src/components/popover/index.tsx +35 -0
- package/src/components/progress/index.tsx +32 -0
- package/src/components/radio-group/index.tsx +48 -0
- package/src/components/scroll-area/index.tsx +52 -0
- package/src/components/select/index.tsx +164 -0
- package/src/components/separator/index.tsx +35 -0
- package/src/components/sheet/index.tsx +147 -0
- package/src/components/skeleton/index.tsx +22 -0
- package/src/components/slider/index.tsx +32 -0
- package/src/components/switch/index.tsx +33 -0
- package/src/components/table/index.tsx +117 -0
- package/src/components/tabs/index.tsx +59 -0
- package/src/components/textarea/index.tsx +30 -0
- package/src/components/theme-selector/index.tsx +327 -0
- package/src/components/toast/index.tsx +133 -0
- package/src/components/toaster/index.tsx +34 -0
- package/src/components/toggle/index.tsx +49 -0
- package/src/components/tooltip/index.tsx +34 -0
- package/src/components/typography/index.tsx +276 -0
- package/src/components/waka-3d-pie-chart/index.tsx +486 -0
- package/src/components/waka-achievement-unlock/index.tsx +716 -0
- package/src/components/waka-activity-feed/index.tsx +686 -0
- package/src/components/waka-address-autocomplete/index.tsx +1202 -0
- package/src/components/waka-admincrumb/index.tsx +349 -0
- package/src/components/waka-alert-stack/index.tsx +827 -0
- package/src/components/waka-allocation-matrix/index.tsx +1278 -0
- package/src/components/waka-approval-chain/index.tsx +766 -0
- package/src/components/waka-audit-log/index.tsx +1475 -0
- package/src/components/waka-autocomplete/index.tsx +358 -0
- package/src/components/waka-badge-showcase/index.tsx +704 -0
- package/src/components/waka-barcode/index.tsx +260 -0
- package/src/components/waka-biometric-prompt/index.tsx +765 -0
- package/src/components/waka-bottom-sheet/index.tsx +495 -0
- package/src/components/waka-breadcrumb/index.tsx +376 -0
- package/src/components/waka-breadcrumb-path/index.tsx +513 -0
- package/src/components/waka-budget-burn/index.tsx +1234 -0
- package/src/components/waka-capacity-planner/index.tsx +1107 -0
- package/src/components/waka-carousel/index.tsx +893 -0
- package/src/components/waka-cart-summary/index.tsx +1055 -0
- package/src/components/waka-challenge-timer/index.tsx +1044 -0
- package/src/components/waka-charts/WakaAreaChart.tsx +251 -0
- package/src/components/waka-charts/WakaBarChart.tsx +222 -0
- package/src/components/waka-charts/WakaChart.tsx +124 -0
- package/src/components/waka-charts/WakaLineChart.tsx +219 -0
- package/src/components/waka-charts/WakaMiniChart.tsx +133 -0
- package/src/components/waka-charts/WakaPieChart.tsx +214 -0
- package/src/components/waka-charts/WakaSparkline.tsx +229 -0
- package/src/components/waka-charts/dataTableHelpers.ts +109 -0
- package/src/components/waka-charts/hooks/useChartTheme.ts +123 -0
- package/src/components/waka-charts/hooks/useRechartsLoader.ts +234 -0
- package/src/components/waka-charts/index.ts +90 -0
- package/src/components/waka-charts/types.ts +330 -0
- package/src/components/waka-chat-bubble/index.tsx +1060 -0
- package/src/components/waka-checklist/index.tsx +1067 -0
- package/src/components/waka-checkout-stepper/index.tsx +976 -0
- package/src/components/waka-cohort-table/index.tsx +1011 -0
- package/src/components/waka-color-picker/index.tsx +447 -0
- package/src/components/waka-combo-counter/index.tsx +864 -0
- package/src/components/waka-combobox/index.tsx +497 -0
- package/src/components/waka-command-bar/index.tsx +403 -0
- package/src/components/waka-compare-period/index.tsx +1230 -0
- package/src/components/waka-connection-matrix/index.tsx +1053 -0
- package/src/components/waka-contribution-graph/index.tsx +552 -0
- package/src/components/waka-cost-breakdown/index.tsx +1065 -0
- package/src/components/waka-coupon-input/index.tsx +592 -0
- package/src/components/waka-credit-card-input/index.tsx +982 -0
- package/src/components/waka-daily-reward/index.tsx +762 -0
- package/src/components/waka-date-range-picker/index.tsx +378 -0
- package/src/components/waka-datetime-picker/index.tsx +793 -0
- package/src/components/waka-datetime-picker.form-integration/index.tsx +402 -0
- package/src/components/waka-deployment-lane/index.tsx +673 -0
- package/src/components/waka-device-trust/index.tsx +1259 -0
- package/src/components/waka-dock/index.tsx +285 -0
- package/src/components/waka-drawer/index.tsx +319 -0
- package/src/components/waka-empty-state/index.tsx +545 -0
- package/src/components/waka-error-shake/index.tsx +398 -0
- package/src/components/waka-feature-announcement/index.tsx +991 -0
- package/src/components/waka-file-upload/index.tsx +437 -0
- package/src/components/waka-floating-nav/index.tsx +413 -0
- package/src/components/waka-flow-diagram/index.tsx +508 -0
- package/src/components/waka-funnel-chart/index.tsx +823 -0
- package/src/components/waka-glow-card/index.tsx +246 -0
- package/src/components/waka-goal-progress/index.tsx +1025 -0
- package/src/components/waka-haptic-button/index.tsx +388 -0
- package/src/components/waka-health-pulse/index.tsx +451 -0
- package/src/components/waka-heatmap/index.tsx +1026 -0
- package/src/components/waka-hotspot/index.tsx +682 -0
- package/src/components/waka-image/index.tsx +373 -0
- package/src/components/waka-incident-timeline/index.tsx +686 -0
- package/src/components/waka-invoice-preview/index.tsx +829 -0
- package/src/components/waka-kanban/index.tsx +646 -0
- package/src/components/waka-kpi-dashboard/index.tsx +755 -0
- package/src/components/waka-leaderboard/index.tsx +746 -0
- package/src/components/waka-level-progress/index.tsx +665 -0
- package/src/components/waka-liquid-button/index.tsx +520 -0
- package/src/components/waka-loading-orbit/index.tsx +478 -0
- package/src/components/waka-loot-box/index.tsx +1091 -0
- package/src/components/waka-magic-link/index.tsx +321 -0
- package/src/components/waka-magnetic-button/index.tsx +567 -0
- package/src/components/waka-mention-input/index.tsx +953 -0
- package/src/components/waka-metric-sparkline/index.tsx +627 -0
- package/src/components/waka-milestone-road/index.tsx +1064 -0
- package/src/components/waka-modal/index.tsx +374 -0
- package/src/components/waka-morph-button/index.tsx +495 -0
- package/src/components/waka-network-topology/index.tsx +801 -0
- package/src/components/waka-notifications/index.tsx +414 -0
- package/src/components/waka-number-input/index.tsx +373 -0
- package/src/components/waka-orbital-menu/index.tsx +445 -0
- package/src/components/waka-order-tracker/index.tsx +1041 -0
- package/src/components/waka-pagination/index.tsx +393 -0
- package/src/components/waka-password-strength/index.tsx +824 -0
- package/src/components/waka-payment-method-picker/index.tsx +715 -0
- package/src/components/waka-permission-matrix/index.tsx +1302 -0
- package/src/components/waka-phone-input/index.tsx +801 -0
- package/src/components/waka-pipeline-view/index.tsx +604 -0
- package/src/components/waka-player-card/index.tsx +691 -0
- package/src/components/waka-points-popup/index.tsx +366 -0
- package/src/components/waka-power-up/index.tsx +1155 -0
- package/src/components/waka-presence-indicator/index.tsx +1181 -0
- package/src/components/waka-pricing-table/index.tsx +755 -0
- package/src/components/waka-product-card/index.tsx +786 -0
- package/src/components/waka-progress-onboarding/index.tsx +878 -0
- package/src/components/waka-pull-to-refresh/index.tsx +451 -0
- package/src/components/waka-qrcode/index.tsx +232 -0
- package/src/components/waka-quest-card/index.tsx +1275 -0
- package/src/components/waka-quota-bar/index.tsx +693 -0
- package/src/components/waka-radar-score/index.tsx +512 -0
- package/src/components/waka-rank-badge/index.tsx +813 -0
- package/src/components/waka-rating-input/index.tsx +560 -0
- package/src/components/waka-reaction-picker/index.tsx +1062 -0
- package/src/components/waka-region-map/index.tsx +730 -0
- package/src/components/waka-resource-gauge/index.tsx +654 -0
- package/src/components/waka-resource-pool/index.tsx +1035 -0
- package/src/components/waka-rich-text-editor/index.tsx +594 -0
- package/src/components/waka-rollback-slider/index.tsx +891 -0
- package/src/components/waka-sankey-diagram/index.tsx +1032 -0
- package/src/components/waka-schedule-picker/index.tsx +1060 -0
- package/src/components/waka-scratch-card/index.tsx +914 -0
- package/src/components/waka-season-pass/index.tsx +886 -0
- package/src/components/waka-security-score/index.tsx +1126 -0
- package/src/components/waka-segmented-control/index.tsx +238 -0
- package/src/components/waka-server-rack/index.tsx +764 -0
- package/src/components/waka-session-manager/index.tsx +815 -0
- package/src/components/waka-signature-pad/index.tsx +744 -0
- package/src/components/waka-skeleton-wave/index.tsx +454 -0
- package/src/components/waka-skill-tree/index.tsx +1031 -0
- package/src/components/waka-sla-tracker/index.tsx +798 -0
- package/src/components/waka-slider-range/index.tsx +765 -0
- package/src/components/waka-spin-wheel/index.tsx +671 -0
- package/src/components/waka-spinner/index.tsx +284 -0
- package/src/components/waka-spotlight/index.tsx +410 -0
- package/src/components/waka-stat/index.tsx +428 -0
- package/src/components/waka-stats-hexagon/index.tsx +824 -0
- package/src/components/waka-status-matrix/index.tsx +565 -0
- package/src/components/waka-stepper/index.tsx +489 -0
- package/src/components/waka-streak-counter/index.tsx +334 -0
- package/src/components/waka-success-explosion/index.tsx +453 -0
- package/src/components/waka-swipe-card/index.tsx +574 -0
- package/src/components/waka-tabs-morph/index.tsx +509 -0
- package/src/components/waka-tag-input/index.tsx +877 -0
- package/src/components/waka-team-banner/index.tsx +1183 -0
- package/src/components/waka-terminal-output/index.tsx +836 -0
- package/src/components/waka-theme-creator/index.tsx +762 -0
- package/src/components/waka-theme-manager/index.tsx +654 -0
- package/src/components/waka-thread-view/index.tsx +874 -0
- package/src/components/waka-tilt-card/index.tsx +250 -0
- package/src/components/waka-time-picker/index.tsx +479 -0
- package/src/components/waka-timeline/index.tsx +385 -0
- package/src/components/waka-tooltip-tour/index.tsx +855 -0
- package/src/components/waka-tour-guide/index.tsx +920 -0
- package/src/components/waka-tournament-bracket/index.tsx +1276 -0
- package/src/components/waka-tree/index.tsx +557 -0
- package/src/components/waka-treemap-chart/index.tsx +1031 -0
- package/src/components/waka-two-factor-setup/index.tsx +995 -0
- package/src/components/waka-typewriter/index.tsx +566 -0
- package/src/components/waka-typing-indicator/index.tsx +649 -0
- package/src/components/waka-versus-card/index.tsx +1026 -0
- package/src/components/waka-video/index.tsx +557 -0
- package/src/components/waka-video-call/index.tsx +1087 -0
- package/src/components/waka-virtual-list/index.tsx +327 -0
- package/src/components/waka-voice-message/index.tsx +1019 -0
- package/src/components/waka-welcome-modal/index.tsx +790 -0
- 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
|