@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,704 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../utils/cn"
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Types
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
export type BadgeRarity = "common" | "rare" | "epic" | "legendary"
|
|
11
|
+
|
|
12
|
+
export interface Badge {
|
|
13
|
+
/** Unique identifier */
|
|
14
|
+
id: string
|
|
15
|
+
/** Badge name */
|
|
16
|
+
name: string
|
|
17
|
+
/** Badge description */
|
|
18
|
+
description: string
|
|
19
|
+
/** Badge icon (React node) */
|
|
20
|
+
icon: React.ReactNode
|
|
21
|
+
/** Rarity tier */
|
|
22
|
+
rarity: BadgeRarity
|
|
23
|
+
/** Whether the badge is unlocked */
|
|
24
|
+
unlocked: boolean
|
|
25
|
+
/** When the badge was unlocked */
|
|
26
|
+
unlockedAt?: Date
|
|
27
|
+
/** Progress towards unlocking (0-100) */
|
|
28
|
+
progress?: number
|
|
29
|
+
/** Badge category */
|
|
30
|
+
category?: string
|
|
31
|
+
/** Whether this is a newly unlocked badge */
|
|
32
|
+
isNew?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface WakaBadgeShowcaseProps {
|
|
36
|
+
/** Array of badges to display */
|
|
37
|
+
badges: Badge[]
|
|
38
|
+
/** Callback when a badge is clicked */
|
|
39
|
+
onBadgeClick?: (id: string) => void
|
|
40
|
+
/** Display variant */
|
|
41
|
+
variant?: "grid" | "list" | "showcase"
|
|
42
|
+
/** Whether to show locked badges */
|
|
43
|
+
showLocked?: boolean
|
|
44
|
+
/** Whether to show progress indicators */
|
|
45
|
+
showProgress?: boolean
|
|
46
|
+
/** Number of columns for grid layout */
|
|
47
|
+
columns?: number
|
|
48
|
+
/** Size of badge icons */
|
|
49
|
+
size?: "sm" | "md" | "lg"
|
|
50
|
+
/** Additional CSS classes */
|
|
51
|
+
className?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Rarity Configuration
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
const rarityConfig = {
|
|
59
|
+
common: {
|
|
60
|
+
gradient: "from-slate-400 to-slate-500",
|
|
61
|
+
glow: "#94a3b8",
|
|
62
|
+
border: "border-slate-400",
|
|
63
|
+
bg: "bg-slate-100 dark:bg-slate-800",
|
|
64
|
+
text: "text-slate-600 dark:text-slate-300",
|
|
65
|
+
shine: "rgba(148, 163, 184, 0.6)",
|
|
66
|
+
label: "Common",
|
|
67
|
+
},
|
|
68
|
+
rare: {
|
|
69
|
+
gradient: "from-blue-400 to-blue-600",
|
|
70
|
+
glow: "#3b82f6",
|
|
71
|
+
border: "border-blue-400",
|
|
72
|
+
bg: "bg-blue-100 dark:bg-blue-900",
|
|
73
|
+
text: "text-blue-600 dark:text-blue-300",
|
|
74
|
+
shine: "rgba(59, 130, 246, 0.6)",
|
|
75
|
+
label: "Rare",
|
|
76
|
+
},
|
|
77
|
+
epic: {
|
|
78
|
+
gradient: "from-purple-400 to-purple-600",
|
|
79
|
+
glow: "#a855f7",
|
|
80
|
+
border: "border-purple-400",
|
|
81
|
+
bg: "bg-purple-100 dark:bg-purple-900",
|
|
82
|
+
text: "text-purple-600 dark:text-purple-300",
|
|
83
|
+
shine: "rgba(168, 85, 247, 0.6)",
|
|
84
|
+
label: "Epic",
|
|
85
|
+
},
|
|
86
|
+
legendary: {
|
|
87
|
+
gradient: "from-amber-400 via-orange-500 to-red-500",
|
|
88
|
+
glow: "#f59e0b",
|
|
89
|
+
border: "border-amber-400",
|
|
90
|
+
bg: "bg-amber-100 dark:bg-amber-900",
|
|
91
|
+
text: "text-amber-600 dark:text-amber-300",
|
|
92
|
+
shine: "rgba(245, 158, 11, 0.8)",
|
|
93
|
+
label: "Legendary",
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============================================================================
|
|
98
|
+
// Size Configuration
|
|
99
|
+
// ============================================================================
|
|
100
|
+
|
|
101
|
+
const sizeConfig = {
|
|
102
|
+
sm: {
|
|
103
|
+
badge: "h-12 w-12",
|
|
104
|
+
icon: "h-6 w-6",
|
|
105
|
+
text: "text-xs",
|
|
106
|
+
gap: "gap-2",
|
|
107
|
+
},
|
|
108
|
+
md: {
|
|
109
|
+
badge: "h-16 w-16",
|
|
110
|
+
icon: "h-8 w-8",
|
|
111
|
+
text: "text-sm",
|
|
112
|
+
gap: "gap-3",
|
|
113
|
+
},
|
|
114
|
+
lg: {
|
|
115
|
+
badge: "h-24 w-24",
|
|
116
|
+
icon: "h-12 w-12",
|
|
117
|
+
text: "text-base",
|
|
118
|
+
gap: "gap-4",
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ============================================================================
|
|
123
|
+
// Shine Effect Component
|
|
124
|
+
// ============================================================================
|
|
125
|
+
|
|
126
|
+
function ShineEffect({ active, color }: { active: boolean; color: string }) {
|
|
127
|
+
if (!active) return null
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<>
|
|
131
|
+
<div className="absolute inset-0 overflow-hidden rounded-full">
|
|
132
|
+
<div
|
|
133
|
+
className="absolute inset-0 animate-badge-shine"
|
|
134
|
+
style={{
|
|
135
|
+
background: `linear-gradient(
|
|
136
|
+
90deg,
|
|
137
|
+
transparent 0%,
|
|
138
|
+
${color} 50%,
|
|
139
|
+
transparent 100%
|
|
140
|
+
)`,
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
<style>{`
|
|
145
|
+
@keyframes badge-shine {
|
|
146
|
+
0% {
|
|
147
|
+
transform: translateX(-100%) rotate(45deg);
|
|
148
|
+
}
|
|
149
|
+
100% {
|
|
150
|
+
transform: translateX(200%) rotate(45deg);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
.animate-badge-shine {
|
|
154
|
+
animation: badge-shine 3s ease-in-out infinite;
|
|
155
|
+
}
|
|
156
|
+
`}</style>
|
|
157
|
+
</>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ============================================================================
|
|
162
|
+
// Glow Effect Component
|
|
163
|
+
// ============================================================================
|
|
164
|
+
|
|
165
|
+
function GlowEffect({ active, color }: { active: boolean; color: string }) {
|
|
166
|
+
if (!active) return null
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<>
|
|
170
|
+
<div
|
|
171
|
+
className="absolute inset-0 rounded-full animate-badge-glow"
|
|
172
|
+
style={{
|
|
173
|
+
boxShadow: `0 0 20px 4px ${color}60, 0 0 40px 8px ${color}30`,
|
|
174
|
+
}}
|
|
175
|
+
/>
|
|
176
|
+
<style>{`
|
|
177
|
+
@keyframes badge-glow {
|
|
178
|
+
0%, 100% { opacity: 0.6; }
|
|
179
|
+
50% { opacity: 1; }
|
|
180
|
+
}
|
|
181
|
+
.animate-badge-glow {
|
|
182
|
+
animation: badge-glow 2s ease-in-out infinite;
|
|
183
|
+
}
|
|
184
|
+
`}</style>
|
|
185
|
+
</>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ============================================================================
|
|
190
|
+
// New Badge Highlight Component
|
|
191
|
+
// ============================================================================
|
|
192
|
+
|
|
193
|
+
function NewBadgeIndicator() {
|
|
194
|
+
return (
|
|
195
|
+
<>
|
|
196
|
+
<div className="absolute -top-1 -right-1 z-20">
|
|
197
|
+
<span className="relative flex h-4 w-4">
|
|
198
|
+
<span className="animate-badge-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
|
199
|
+
<span className="relative inline-flex rounded-full h-4 w-4 bg-green-500 items-center justify-center">
|
|
200
|
+
<span className="text-[8px] font-bold text-white">!</span>
|
|
201
|
+
</span>
|
|
202
|
+
</span>
|
|
203
|
+
</div>
|
|
204
|
+
<style>{`
|
|
205
|
+
@keyframes badge-ping {
|
|
206
|
+
75%, 100% {
|
|
207
|
+
transform: scale(2);
|
|
208
|
+
opacity: 0;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
.animate-badge-ping {
|
|
212
|
+
animation: badge-ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
|
|
213
|
+
}
|
|
214
|
+
`}</style>
|
|
215
|
+
</>
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ============================================================================
|
|
220
|
+
// Progress Ring Component
|
|
221
|
+
// ============================================================================
|
|
222
|
+
|
|
223
|
+
function ProgressRing({
|
|
224
|
+
progress,
|
|
225
|
+
color,
|
|
226
|
+
size,
|
|
227
|
+
}: {
|
|
228
|
+
progress: number
|
|
229
|
+
color: string
|
|
230
|
+
size: "sm" | "md" | "lg"
|
|
231
|
+
}) {
|
|
232
|
+
const strokeWidth = size === "sm" ? 3 : size === "md" ? 4 : 5
|
|
233
|
+
const radius = 46
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<svg className="absolute inset-0 -rotate-90" viewBox="0 0 100 100">
|
|
237
|
+
<circle
|
|
238
|
+
cx="50"
|
|
239
|
+
cy="50"
|
|
240
|
+
r={radius}
|
|
241
|
+
fill="none"
|
|
242
|
+
stroke="currentColor"
|
|
243
|
+
strokeWidth={strokeWidth}
|
|
244
|
+
className="text-muted-foreground/20"
|
|
245
|
+
/>
|
|
246
|
+
<circle
|
|
247
|
+
cx="50"
|
|
248
|
+
cy="50"
|
|
249
|
+
r={radius}
|
|
250
|
+
fill="none"
|
|
251
|
+
stroke={color}
|
|
252
|
+
strokeWidth={strokeWidth}
|
|
253
|
+
strokeLinecap="round"
|
|
254
|
+
strokeDasharray={`${(progress / 100) * 2 * Math.PI * radius} ${2 * Math.PI * radius}`}
|
|
255
|
+
className="transition-all duration-500"
|
|
256
|
+
/>
|
|
257
|
+
</svg>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ============================================================================
|
|
262
|
+
// Badge Item Component
|
|
263
|
+
// ============================================================================
|
|
264
|
+
|
|
265
|
+
interface BadgeItemProps {
|
|
266
|
+
badge: Badge
|
|
267
|
+
size: "sm" | "md" | "lg"
|
|
268
|
+
showProgress: boolean
|
|
269
|
+
onClick?: () => void
|
|
270
|
+
variant: "grid" | "list" | "showcase"
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function BadgeItem({ badge, size, showProgress, onClick, variant }: BadgeItemProps) {
|
|
274
|
+
const [isHovered, setIsHovered] = React.useState(false)
|
|
275
|
+
const config = rarityConfig[badge.rarity]
|
|
276
|
+
const sizes = sizeConfig[size]
|
|
277
|
+
|
|
278
|
+
const isLocked = !badge.unlocked
|
|
279
|
+
const hasProgress = isLocked && showProgress && badge.progress !== undefined && badge.progress > 0
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<div
|
|
283
|
+
className={cn(
|
|
284
|
+
"group relative cursor-pointer transition-transform duration-200",
|
|
285
|
+
variant === "list" && "flex items-center gap-4",
|
|
286
|
+
variant === "showcase" && "flex flex-col items-center",
|
|
287
|
+
onClick && "cursor-pointer"
|
|
288
|
+
)}
|
|
289
|
+
onClick={onClick}
|
|
290
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
291
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
292
|
+
>
|
|
293
|
+
{/* Badge Container */}
|
|
294
|
+
<div className="relative">
|
|
295
|
+
{/* Glow effect for unlocked badges */}
|
|
296
|
+
{!isLocked && <GlowEffect active={isHovered} color={config.glow} />}
|
|
297
|
+
|
|
298
|
+
{/* New badge indicator */}
|
|
299
|
+
{badge.isNew && !isLocked && <NewBadgeIndicator />}
|
|
300
|
+
|
|
301
|
+
{/* Main badge circle */}
|
|
302
|
+
<div
|
|
303
|
+
className={cn(
|
|
304
|
+
"relative flex items-center justify-center rounded-full border-2 transition-all duration-300",
|
|
305
|
+
sizes.badge,
|
|
306
|
+
isLocked
|
|
307
|
+
? "bg-muted/50 border-muted-foreground/30 grayscale"
|
|
308
|
+
: cn("bg-gradient-to-br", config.gradient, config.border),
|
|
309
|
+
!isLocked && isHovered && "scale-110",
|
|
310
|
+
!isLocked && "shadow-lg"
|
|
311
|
+
)}
|
|
312
|
+
style={
|
|
313
|
+
!isLocked && isHovered
|
|
314
|
+
? { boxShadow: `0 0 30px 8px ${config.glow}50` }
|
|
315
|
+
: {}
|
|
316
|
+
}
|
|
317
|
+
>
|
|
318
|
+
{/* Progress ring for locked badges */}
|
|
319
|
+
{hasProgress && (
|
|
320
|
+
<ProgressRing
|
|
321
|
+
progress={badge.progress!}
|
|
322
|
+
color={config.glow}
|
|
323
|
+
size={size}
|
|
324
|
+
/>
|
|
325
|
+
)}
|
|
326
|
+
|
|
327
|
+
{/* Icon or silhouette */}
|
|
328
|
+
<div
|
|
329
|
+
className={cn(
|
|
330
|
+
sizes.icon,
|
|
331
|
+
"transition-all duration-200 flex items-center justify-center",
|
|
332
|
+
isLocked
|
|
333
|
+
? "text-muted-foreground/40 opacity-50 blur-[1px]"
|
|
334
|
+
: "text-white"
|
|
335
|
+
)}
|
|
336
|
+
>
|
|
337
|
+
{badge.icon}
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
{/* Lock overlay for locked badges */}
|
|
341
|
+
{isLocked && (
|
|
342
|
+
<div className="absolute inset-0 flex items-center justify-center rounded-full bg-black/20">
|
|
343
|
+
<svg
|
|
344
|
+
className="h-5 w-5 text-muted-foreground/60"
|
|
345
|
+
fill="none"
|
|
346
|
+
viewBox="0 0 24 24"
|
|
347
|
+
stroke="currentColor"
|
|
348
|
+
>
|
|
349
|
+
<path
|
|
350
|
+
strokeLinecap="round"
|
|
351
|
+
strokeLinejoin="round"
|
|
352
|
+
strokeWidth={2}
|
|
353
|
+
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
|
354
|
+
/>
|
|
355
|
+
</svg>
|
|
356
|
+
</div>
|
|
357
|
+
)}
|
|
358
|
+
|
|
359
|
+
{/* Shine effect for unlocked badges */}
|
|
360
|
+
{!isLocked && <ShineEffect active={true} color={config.shine} />}
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
|
|
364
|
+
{/* Badge info for list/showcase variants */}
|
|
365
|
+
{(variant === "list" || variant === "showcase") && (
|
|
366
|
+
<div
|
|
367
|
+
className={cn(
|
|
368
|
+
"transition-opacity duration-200",
|
|
369
|
+
variant === "showcase" && "text-center mt-2",
|
|
370
|
+
isLocked && "opacity-60"
|
|
371
|
+
)}
|
|
372
|
+
>
|
|
373
|
+
<div className={cn("font-semibold", sizes.text)}>{badge.name}</div>
|
|
374
|
+
{variant === "list" && (
|
|
375
|
+
<div className={cn("text-muted-foreground", sizes.text, "text-xs")}>
|
|
376
|
+
{badge.description}
|
|
377
|
+
</div>
|
|
378
|
+
)}
|
|
379
|
+
<div
|
|
380
|
+
className={cn(
|
|
381
|
+
"inline-flex items-center rounded-full px-2 py-0.5 mt-1",
|
|
382
|
+
sizes.text,
|
|
383
|
+
"text-[10px] font-medium uppercase tracking-wider",
|
|
384
|
+
config.bg,
|
|
385
|
+
config.text
|
|
386
|
+
)}
|
|
387
|
+
>
|
|
388
|
+
{config.label}
|
|
389
|
+
</div>
|
|
390
|
+
{hasProgress && (
|
|
391
|
+
<div className={cn("text-muted-foreground mt-1", "text-xs")}>
|
|
392
|
+
{Math.round(badge.progress!)}% complete
|
|
393
|
+
</div>
|
|
394
|
+
)}
|
|
395
|
+
</div>
|
|
396
|
+
)}
|
|
397
|
+
|
|
398
|
+
{/* Hover tooltip for grid variant */}
|
|
399
|
+
{variant === "grid" && isHovered && (
|
|
400
|
+
<div
|
|
401
|
+
className={cn(
|
|
402
|
+
"absolute left-1/2 top-full z-30 mt-2 -translate-x-1/2",
|
|
403
|
+
"whitespace-nowrap rounded-lg bg-popover px-3 py-2 text-sm shadow-lg border",
|
|
404
|
+
"animate-badge-tooltip"
|
|
405
|
+
)}
|
|
406
|
+
>
|
|
407
|
+
<div className="font-semibold">{badge.name}</div>
|
|
408
|
+
<div className="text-xs text-muted-foreground max-w-[200px] whitespace-normal">
|
|
409
|
+
{badge.description}
|
|
410
|
+
</div>
|
|
411
|
+
<div
|
|
412
|
+
className={cn(
|
|
413
|
+
"inline-flex items-center rounded-full px-2 py-0.5 mt-1",
|
|
414
|
+
"text-[10px] font-medium uppercase tracking-wider",
|
|
415
|
+
config.bg,
|
|
416
|
+
config.text
|
|
417
|
+
)}
|
|
418
|
+
>
|
|
419
|
+
{config.label}
|
|
420
|
+
</div>
|
|
421
|
+
{badge.unlockedAt && !isLocked && (
|
|
422
|
+
<div className="text-xs text-muted-foreground mt-1">
|
|
423
|
+
Unlocked: {badge.unlockedAt.toLocaleDateString()}
|
|
424
|
+
</div>
|
|
425
|
+
)}
|
|
426
|
+
{hasProgress && (
|
|
427
|
+
<div className="text-xs text-muted-foreground mt-1">
|
|
428
|
+
Progress: {Math.round(badge.progress!)}%
|
|
429
|
+
</div>
|
|
430
|
+
)}
|
|
431
|
+
<style>{`
|
|
432
|
+
@keyframes badge-tooltip {
|
|
433
|
+
from { opacity: 0; transform: translate(-50%, 10px); }
|
|
434
|
+
to { opacity: 1; transform: translate(-50%, 0); }
|
|
435
|
+
}
|
|
436
|
+
.animate-badge-tooltip {
|
|
437
|
+
animation: badge-tooltip 0.2s ease-out;
|
|
438
|
+
}
|
|
439
|
+
`}</style>
|
|
440
|
+
</div>
|
|
441
|
+
)}
|
|
442
|
+
</div>
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ============================================================================
|
|
447
|
+
// Category Header Component
|
|
448
|
+
// ============================================================================
|
|
449
|
+
|
|
450
|
+
function CategoryHeader({
|
|
451
|
+
category,
|
|
452
|
+
count,
|
|
453
|
+
unlockedCount,
|
|
454
|
+
}: {
|
|
455
|
+
category: string
|
|
456
|
+
count: number
|
|
457
|
+
unlockedCount: number
|
|
458
|
+
}) {
|
|
459
|
+
return (
|
|
460
|
+
<div className="flex items-center justify-between mb-4">
|
|
461
|
+
<h3 className="text-lg font-semibold capitalize">{category}</h3>
|
|
462
|
+
<span className="text-sm text-muted-foreground">
|
|
463
|
+
{unlockedCount}/{count}
|
|
464
|
+
</span>
|
|
465
|
+
</div>
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ============================================================================
|
|
470
|
+
// Main Component
|
|
471
|
+
// ============================================================================
|
|
472
|
+
|
|
473
|
+
export function WakaBadgeShowcase({
|
|
474
|
+
badges,
|
|
475
|
+
onBadgeClick,
|
|
476
|
+
variant = "grid",
|
|
477
|
+
showLocked = true,
|
|
478
|
+
showProgress = true,
|
|
479
|
+
columns = 4,
|
|
480
|
+
size = "md",
|
|
481
|
+
className,
|
|
482
|
+
}: WakaBadgeShowcaseProps) {
|
|
483
|
+
// Filter badges based on showLocked
|
|
484
|
+
const displayBadges = showLocked ? badges : badges.filter((b) => b.unlocked)
|
|
485
|
+
|
|
486
|
+
// Group badges by category if categories exist
|
|
487
|
+
const categories = React.useMemo(() => {
|
|
488
|
+
const grouped: Record<string, Badge[]> = {}
|
|
489
|
+
displayBadges.forEach((badge) => {
|
|
490
|
+
const cat = badge.category || "uncategorized"
|
|
491
|
+
if (!grouped[cat]) grouped[cat] = []
|
|
492
|
+
grouped[cat].push(badge)
|
|
493
|
+
})
|
|
494
|
+
return grouped
|
|
495
|
+
}, [displayBadges])
|
|
496
|
+
|
|
497
|
+
const hasCategories = Object.keys(categories).length > 1 || !categories["uncategorized"]
|
|
498
|
+
|
|
499
|
+
// Grid column classes
|
|
500
|
+
const getGridCols = () => {
|
|
501
|
+
switch (columns) {
|
|
502
|
+
case 1:
|
|
503
|
+
return "grid-cols-1"
|
|
504
|
+
case 2:
|
|
505
|
+
return "grid-cols-2"
|
|
506
|
+
case 3:
|
|
507
|
+
return "grid-cols-3"
|
|
508
|
+
case 4:
|
|
509
|
+
return "grid-cols-4"
|
|
510
|
+
case 5:
|
|
511
|
+
return "grid-cols-5"
|
|
512
|
+
case 6:
|
|
513
|
+
return "grid-cols-6"
|
|
514
|
+
default:
|
|
515
|
+
return "grid-cols-4"
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Render badges without categories
|
|
520
|
+
const renderBadges = (badgesToRender: Badge[]) => {
|
|
521
|
+
if (variant === "list") {
|
|
522
|
+
return (
|
|
523
|
+
<div className="flex flex-col gap-4">
|
|
524
|
+
{badgesToRender.map((badge) => (
|
|
525
|
+
<BadgeItem
|
|
526
|
+
key={badge.id}
|
|
527
|
+
badge={badge}
|
|
528
|
+
size={size}
|
|
529
|
+
showProgress={showProgress}
|
|
530
|
+
onClick={onBadgeClick ? () => onBadgeClick(badge.id) : undefined}
|
|
531
|
+
variant={variant}
|
|
532
|
+
/>
|
|
533
|
+
))}
|
|
534
|
+
</div>
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (variant === "showcase") {
|
|
539
|
+
return (
|
|
540
|
+
<div className="flex flex-wrap justify-center gap-8">
|
|
541
|
+
{badgesToRender.map((badge) => (
|
|
542
|
+
<BadgeItem
|
|
543
|
+
key={badge.id}
|
|
544
|
+
badge={badge}
|
|
545
|
+
size={size}
|
|
546
|
+
showProgress={showProgress}
|
|
547
|
+
onClick={onBadgeClick ? () => onBadgeClick(badge.id) : undefined}
|
|
548
|
+
variant={variant}
|
|
549
|
+
/>
|
|
550
|
+
))}
|
|
551
|
+
</div>
|
|
552
|
+
)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Grid variant (default)
|
|
556
|
+
return (
|
|
557
|
+
<div className={cn("grid gap-6", getGridCols())}>
|
|
558
|
+
{badgesToRender.map((badge) => (
|
|
559
|
+
<div key={badge.id} className="flex justify-center">
|
|
560
|
+
<BadgeItem
|
|
561
|
+
badge={badge}
|
|
562
|
+
size={size}
|
|
563
|
+
showProgress={showProgress}
|
|
564
|
+
onClick={onBadgeClick ? () => onBadgeClick(badge.id) : undefined}
|
|
565
|
+
variant={variant}
|
|
566
|
+
/>
|
|
567
|
+
</div>
|
|
568
|
+
))}
|
|
569
|
+
</div>
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return (
|
|
574
|
+
<div className={cn("relative", className)}>
|
|
575
|
+
{hasCategories ? (
|
|
576
|
+
<div className="space-y-8">
|
|
577
|
+
{Object.entries(categories).map(([category, categoryBadges]) => (
|
|
578
|
+
<div key={category}>
|
|
579
|
+
<CategoryHeader
|
|
580
|
+
category={category}
|
|
581
|
+
count={categoryBadges.length}
|
|
582
|
+
unlockedCount={categoryBadges.filter((b) => b.unlocked).length}
|
|
583
|
+
/>
|
|
584
|
+
{renderBadges(categoryBadges)}
|
|
585
|
+
</div>
|
|
586
|
+
))}
|
|
587
|
+
</div>
|
|
588
|
+
) : (
|
|
589
|
+
renderBadges(displayBadges)
|
|
590
|
+
)}
|
|
591
|
+
|
|
592
|
+
{/* Global animations */}
|
|
593
|
+
<style>{`
|
|
594
|
+
@keyframes legendary-shimmer {
|
|
595
|
+
0% { background-position: 0% 50%; }
|
|
596
|
+
50% { background-position: 100% 50%; }
|
|
597
|
+
100% { background-position: 0% 50%; }
|
|
598
|
+
}
|
|
599
|
+
`}</style>
|
|
600
|
+
</div>
|
|
601
|
+
)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ============================================================================
|
|
605
|
+
// Preset Showcase Variants
|
|
606
|
+
// ============================================================================
|
|
607
|
+
|
|
608
|
+
export function WakaBadgeShowcaseCompact(
|
|
609
|
+
props: Omit<WakaBadgeShowcaseProps, "variant" | "size" | "columns">
|
|
610
|
+
) {
|
|
611
|
+
return <WakaBadgeShowcase variant="grid" size="sm" columns={6} {...props} />
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export function WakaBadgeShowcaseList(
|
|
615
|
+
props: Omit<WakaBadgeShowcaseProps, "variant">
|
|
616
|
+
) {
|
|
617
|
+
return <WakaBadgeShowcase variant="list" {...props} />
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export function WakaBadgeShowcaseHero(
|
|
621
|
+
props: Omit<WakaBadgeShowcaseProps, "variant" | "size">
|
|
622
|
+
) {
|
|
623
|
+
return <WakaBadgeShowcase variant="showcase" size="lg" {...props} />
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ============================================================================
|
|
627
|
+
// Badge Stats Component
|
|
628
|
+
// ============================================================================
|
|
629
|
+
|
|
630
|
+
export interface WakaBadgeStatsProps {
|
|
631
|
+
badges: Badge[]
|
|
632
|
+
className?: string
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export function WakaBadgeStats({ badges, className }: WakaBadgeStatsProps) {
|
|
636
|
+
const stats = React.useMemo(() => {
|
|
637
|
+
const total = badges.length
|
|
638
|
+
const unlocked = badges.filter((b) => b.unlocked).length
|
|
639
|
+
const byRarity = {
|
|
640
|
+
common: badges.filter((b) => b.rarity === "common"),
|
|
641
|
+
rare: badges.filter((b) => b.rarity === "rare"),
|
|
642
|
+
epic: badges.filter((b) => b.rarity === "epic"),
|
|
643
|
+
legendary: badges.filter((b) => b.rarity === "legendary"),
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return {
|
|
647
|
+
total,
|
|
648
|
+
unlocked,
|
|
649
|
+
percentage: Math.round((unlocked / total) * 100),
|
|
650
|
+
byRarity,
|
|
651
|
+
}
|
|
652
|
+
}, [badges])
|
|
653
|
+
|
|
654
|
+
return (
|
|
655
|
+
<div
|
|
656
|
+
className={cn(
|
|
657
|
+
"flex flex-wrap gap-4 p-4 rounded-lg bg-muted/50 border",
|
|
658
|
+
className
|
|
659
|
+
)}
|
|
660
|
+
>
|
|
661
|
+
<div className="flex-1 min-w-[100px]">
|
|
662
|
+
<div className="text-2xl font-bold">
|
|
663
|
+
{stats.unlocked}/{stats.total}
|
|
664
|
+
</div>
|
|
665
|
+
<div className="text-sm text-muted-foreground">Badges Unlocked</div>
|
|
666
|
+
<div className="mt-2 h-2 rounded-full bg-muted overflow-hidden">
|
|
667
|
+
<div
|
|
668
|
+
className="h-full bg-primary rounded-full transition-all duration-500"
|
|
669
|
+
style={{ width: `${stats.percentage}%` }}
|
|
670
|
+
/>
|
|
671
|
+
</div>
|
|
672
|
+
</div>
|
|
673
|
+
|
|
674
|
+
{(["common", "rare", "epic", "legendary"] as const).map((rarity) => {
|
|
675
|
+
const rarityBadges = stats.byRarity[rarity]
|
|
676
|
+
const unlockedCount = rarityBadges.filter((b) => b.unlocked).length
|
|
677
|
+
const config = rarityConfig[rarity]
|
|
678
|
+
|
|
679
|
+
return (
|
|
680
|
+
<div key={rarity} className="flex items-center gap-2">
|
|
681
|
+
<div
|
|
682
|
+
className={cn(
|
|
683
|
+
"h-8 w-8 rounded-full bg-gradient-to-br flex items-center justify-center",
|
|
684
|
+
config.gradient
|
|
685
|
+
)}
|
|
686
|
+
>
|
|
687
|
+
<span className="text-white text-xs font-bold">
|
|
688
|
+
{unlockedCount}
|
|
689
|
+
</span>
|
|
690
|
+
</div>
|
|
691
|
+
<div>
|
|
692
|
+
<div className={cn("text-xs font-medium", config.text)}>
|
|
693
|
+
{config.label}
|
|
694
|
+
</div>
|
|
695
|
+
<div className="text-xs text-muted-foreground">
|
|
696
|
+
of {rarityBadges.length}
|
|
697
|
+
</div>
|
|
698
|
+
</div>
|
|
699
|
+
</div>
|
|
700
|
+
)
|
|
701
|
+
})}
|
|
702
|
+
</div>
|
|
703
|
+
)
|
|
704
|
+
}
|