@wakastellar/ui 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (290) hide show
  1. package/dist/cli/commands/add.d.ts +7 -0
  2. package/dist/cli/commands/init.d.ts +6 -0
  3. package/dist/cli/commands/list.d.ts +5 -0
  4. package/dist/cli/commands/search.d.ts +1 -0
  5. package/dist/cli/index.cjs +4844 -0
  6. package/dist/cli/index.d.ts +1 -0
  7. package/dist/cli/utils/config.d.ts +29 -0
  8. package/dist/cli/utils/logger.d.ts +20 -0
  9. package/dist/cli/utils/registry.d.ts +23 -0
  10. package/package.json +14 -3
  11. package/src/blocks/activity-timeline/index.tsx +586 -0
  12. package/src/blocks/calendar-view/index.tsx +756 -0
  13. package/src/blocks/chat/index.tsx +1018 -0
  14. package/src/blocks/chat/widget.tsx +504 -0
  15. package/src/blocks/dashboard/index.tsx +522 -0
  16. package/src/blocks/empty-states/index.tsx +452 -0
  17. package/src/blocks/error-pages/index.tsx +426 -0
  18. package/src/blocks/faq/index.tsx +479 -0
  19. package/src/blocks/file-manager/index.tsx +890 -0
  20. package/src/blocks/footer/index.tsx +133 -0
  21. package/src/blocks/header/index.tsx +357 -0
  22. package/src/blocks/headtab/index.tsx +139 -0
  23. package/src/blocks/i18n-editor/index.tsx +1016 -0
  24. package/src/blocks/index.ts +80 -0
  25. package/src/blocks/kanban-board/index.tsx +779 -0
  26. package/src/blocks/landing/index.tsx +677 -0
  27. package/src/blocks/language-selector/index.tsx +88 -0
  28. package/src/blocks/layout/index.tsx +159 -0
  29. package/src/blocks/login/index.tsx +339 -0
  30. package/src/blocks/login/types.ts +131 -0
  31. package/src/blocks/pricing/index.tsx +564 -0
  32. package/src/blocks/profile/index.tsx +746 -0
  33. package/src/blocks/settings/index.tsx +558 -0
  34. package/src/blocks/sidebar/index.tsx +713 -0
  35. package/src/blocks/theme-creator-block/index.tsx +835 -0
  36. package/src/blocks/user-management/index.tsx +1037 -0
  37. package/src/blocks/wizard/index.tsx +719 -0
  38. package/src/components/DataTable/DataTable.tsx +406 -0
  39. package/src/components/DataTable/DataTableAdvanced.tsx +720 -0
  40. package/src/components/DataTable/DataTableBody.tsx +216 -0
  41. package/src/components/DataTable/DataTableCell.tsx +172 -0
  42. package/src/components/DataTable/DataTableColumnResizer.tsx +62 -0
  43. package/src/components/DataTable/DataTableConflictResolver.tsx +478 -0
  44. package/src/components/DataTable/DataTableContextMenu.tsx +219 -0
  45. package/src/components/DataTable/DataTableEditCell.tsx +279 -0
  46. package/src/components/DataTable/DataTableFilterBuilder.tsx +519 -0
  47. package/src/components/DataTable/DataTableFilters.tsx +535 -0
  48. package/src/components/DataTable/DataTableGrouping.tsx +147 -0
  49. package/src/components/DataTable/DataTableHeader.tsx +172 -0
  50. package/src/components/DataTable/DataTablePagination.tsx +125 -0
  51. package/src/components/DataTable/DataTableSelection.tsx +269 -0
  52. package/src/components/DataTable/DataTableSyncStatus.tsx +281 -0
  53. package/src/components/DataTable/DataTableToolbar.tsx +262 -0
  54. package/src/components/DataTable/README.md +446 -0
  55. package/src/components/DataTable/__tests__/DataTableAdvanced.test.tsx +426 -0
  56. package/src/components/DataTable/__tests__/DataTableEdit.test.tsx +329 -0
  57. package/src/components/DataTable/__tests__/useDataTableAdvanced.test.ts +455 -0
  58. package/src/components/DataTable/examples/EditExample.tsx +166 -0
  59. package/src/components/DataTable/formatters/index.ts +335 -0
  60. package/src/components/DataTable/hooks/__tests__/useDataTableEdit.test.ts +239 -0
  61. package/src/components/DataTable/hooks/useDataTable.ts +145 -0
  62. package/src/components/DataTable/hooks/useDataTableAdvanced.ts +342 -0
  63. package/src/components/DataTable/hooks/useDataTableAdvancedFilters.ts +637 -0
  64. package/src/components/DataTable/hooks/useDataTableColumnTemplates.ts +186 -0
  65. package/src/components/DataTable/hooks/useDataTableEdit.ts +167 -0
  66. package/src/components/DataTable/hooks/useDataTableExport.ts +227 -0
  67. package/src/components/DataTable/hooks/useDataTableImport.ts +216 -0
  68. package/src/components/DataTable/hooks/useDataTableOffline.ts +481 -0
  69. package/src/components/DataTable/hooks/useDataTableTheme.ts +213 -0
  70. package/src/components/DataTable/hooks/useDataTableVirtualization.ts +99 -0
  71. package/src/components/DataTable/hooks/useTableLayout.ts +85 -0
  72. package/src/components/DataTable/index.ts +81 -0
  73. package/src/components/DataTable/services/IndexedDBService.ts +504 -0
  74. package/src/components/DataTable/templates/index.tsx +803 -0
  75. package/src/components/DataTable/types.ts +504 -0
  76. package/src/components/DataTable/utils.ts +164 -0
  77. package/src/components/DataTable/workers/exportWorker.ts +213 -0
  78. package/src/components/accordion/index.tsx +61 -0
  79. package/src/components/alert/index.tsx +61 -0
  80. package/src/components/alert-dialog/index.tsx +146 -0
  81. package/src/components/aspect-ratio/index.tsx +12 -0
  82. package/src/components/avatar/index.tsx +54 -0
  83. package/src/components/badge/Badge.stories.tsx +64 -0
  84. package/src/components/badge/index.tsx +38 -0
  85. package/src/components/button/Button.stories.tsx +173 -0
  86. package/src/components/button/index.tsx +56 -0
  87. package/src/components/calendar/index.tsx +73 -0
  88. package/src/components/card/index.tsx +78 -0
  89. package/src/components/checkbox/index.tsx +34 -0
  90. package/src/components/code/index.tsx +229 -0
  91. package/src/components/collapsible/index.tsx +16 -0
  92. package/src/components/command/index.tsx +162 -0
  93. package/src/components/context-menu/index.tsx +204 -0
  94. package/src/components/dialog/index.tsx +126 -0
  95. package/src/components/dropdown-menu/index.tsx +204 -0
  96. package/src/components/error-boundary/ErrorBoundary.tsx +281 -0
  97. package/src/components/error-boundary/index.ts +7 -0
  98. package/src/components/form/index.tsx +183 -0
  99. package/src/components/hover-card/index.tsx +33 -0
  100. package/src/components/index.ts +368 -0
  101. package/src/components/input/Input.stories.tsx +100 -0
  102. package/src/components/input/index.tsx +27 -0
  103. package/src/components/input-otp/index.tsx +277 -0
  104. package/src/components/label/index.tsx +30 -0
  105. package/src/components/language-selector/index.tsx +341 -0
  106. package/src/components/menubar/index.tsx +240 -0
  107. package/src/components/navigation-menu/index.tsx +134 -0
  108. package/src/components/popover/index.tsx +35 -0
  109. package/src/components/progress/index.tsx +32 -0
  110. package/src/components/radio-group/index.tsx +48 -0
  111. package/src/components/scroll-area/index.tsx +52 -0
  112. package/src/components/select/index.tsx +164 -0
  113. package/src/components/separator/index.tsx +35 -0
  114. package/src/components/sheet/index.tsx +147 -0
  115. package/src/components/skeleton/index.tsx +22 -0
  116. package/src/components/slider/index.tsx +32 -0
  117. package/src/components/switch/index.tsx +33 -0
  118. package/src/components/table/index.tsx +117 -0
  119. package/src/components/tabs/index.tsx +59 -0
  120. package/src/components/textarea/index.tsx +30 -0
  121. package/src/components/theme-selector/index.tsx +327 -0
  122. package/src/components/toast/index.tsx +133 -0
  123. package/src/components/toaster/index.tsx +34 -0
  124. package/src/components/toggle/index.tsx +49 -0
  125. package/src/components/tooltip/index.tsx +34 -0
  126. package/src/components/typography/index.tsx +276 -0
  127. package/src/components/waka-3d-pie-chart/index.tsx +486 -0
  128. package/src/components/waka-achievement-unlock/index.tsx +716 -0
  129. package/src/components/waka-activity-feed/index.tsx +686 -0
  130. package/src/components/waka-address-autocomplete/index.tsx +1202 -0
  131. package/src/components/waka-admincrumb/index.tsx +349 -0
  132. package/src/components/waka-alert-stack/index.tsx +827 -0
  133. package/src/components/waka-allocation-matrix/index.tsx +1278 -0
  134. package/src/components/waka-approval-chain/index.tsx +766 -0
  135. package/src/components/waka-audit-log/index.tsx +1475 -0
  136. package/src/components/waka-autocomplete/index.tsx +358 -0
  137. package/src/components/waka-badge-showcase/index.tsx +704 -0
  138. package/src/components/waka-barcode/index.tsx +260 -0
  139. package/src/components/waka-biometric-prompt/index.tsx +765 -0
  140. package/src/components/waka-bottom-sheet/index.tsx +495 -0
  141. package/src/components/waka-breadcrumb/index.tsx +376 -0
  142. package/src/components/waka-breadcrumb-path/index.tsx +513 -0
  143. package/src/components/waka-budget-burn/index.tsx +1234 -0
  144. package/src/components/waka-capacity-planner/index.tsx +1107 -0
  145. package/src/components/waka-carousel/index.tsx +893 -0
  146. package/src/components/waka-cart-summary/index.tsx +1055 -0
  147. package/src/components/waka-challenge-timer/index.tsx +1044 -0
  148. package/src/components/waka-charts/WakaAreaChart.tsx +251 -0
  149. package/src/components/waka-charts/WakaBarChart.tsx +222 -0
  150. package/src/components/waka-charts/WakaChart.tsx +124 -0
  151. package/src/components/waka-charts/WakaLineChart.tsx +219 -0
  152. package/src/components/waka-charts/WakaMiniChart.tsx +133 -0
  153. package/src/components/waka-charts/WakaPieChart.tsx +214 -0
  154. package/src/components/waka-charts/WakaSparkline.tsx +229 -0
  155. package/src/components/waka-charts/dataTableHelpers.ts +109 -0
  156. package/src/components/waka-charts/hooks/useChartTheme.ts +123 -0
  157. package/src/components/waka-charts/hooks/useRechartsLoader.ts +234 -0
  158. package/src/components/waka-charts/index.ts +90 -0
  159. package/src/components/waka-charts/types.ts +330 -0
  160. package/src/components/waka-chat-bubble/index.tsx +1060 -0
  161. package/src/components/waka-checklist/index.tsx +1067 -0
  162. package/src/components/waka-checkout-stepper/index.tsx +976 -0
  163. package/src/components/waka-cohort-table/index.tsx +1011 -0
  164. package/src/components/waka-color-picker/index.tsx +447 -0
  165. package/src/components/waka-combo-counter/index.tsx +864 -0
  166. package/src/components/waka-combobox/index.tsx +497 -0
  167. package/src/components/waka-command-bar/index.tsx +403 -0
  168. package/src/components/waka-compare-period/index.tsx +1230 -0
  169. package/src/components/waka-connection-matrix/index.tsx +1053 -0
  170. package/src/components/waka-contribution-graph/index.tsx +552 -0
  171. package/src/components/waka-cost-breakdown/index.tsx +1065 -0
  172. package/src/components/waka-coupon-input/index.tsx +592 -0
  173. package/src/components/waka-credit-card-input/index.tsx +982 -0
  174. package/src/components/waka-daily-reward/index.tsx +762 -0
  175. package/src/components/waka-date-range-picker/index.tsx +378 -0
  176. package/src/components/waka-datetime-picker/index.tsx +793 -0
  177. package/src/components/waka-datetime-picker.form-integration/index.tsx +402 -0
  178. package/src/components/waka-deployment-lane/index.tsx +673 -0
  179. package/src/components/waka-device-trust/index.tsx +1259 -0
  180. package/src/components/waka-dock/index.tsx +285 -0
  181. package/src/components/waka-drawer/index.tsx +319 -0
  182. package/src/components/waka-empty-state/index.tsx +545 -0
  183. package/src/components/waka-error-shake/index.tsx +398 -0
  184. package/src/components/waka-feature-announcement/index.tsx +991 -0
  185. package/src/components/waka-file-upload/index.tsx +437 -0
  186. package/src/components/waka-floating-nav/index.tsx +413 -0
  187. package/src/components/waka-flow-diagram/index.tsx +508 -0
  188. package/src/components/waka-funnel-chart/index.tsx +823 -0
  189. package/src/components/waka-glow-card/index.tsx +246 -0
  190. package/src/components/waka-goal-progress/index.tsx +1025 -0
  191. package/src/components/waka-haptic-button/index.tsx +388 -0
  192. package/src/components/waka-health-pulse/index.tsx +451 -0
  193. package/src/components/waka-heatmap/index.tsx +1026 -0
  194. package/src/components/waka-hotspot/index.tsx +682 -0
  195. package/src/components/waka-image/index.tsx +373 -0
  196. package/src/components/waka-incident-timeline/index.tsx +686 -0
  197. package/src/components/waka-invoice-preview/index.tsx +829 -0
  198. package/src/components/waka-kanban/index.tsx +646 -0
  199. package/src/components/waka-kpi-dashboard/index.tsx +755 -0
  200. package/src/components/waka-leaderboard/index.tsx +746 -0
  201. package/src/components/waka-level-progress/index.tsx +665 -0
  202. package/src/components/waka-liquid-button/index.tsx +520 -0
  203. package/src/components/waka-loading-orbit/index.tsx +478 -0
  204. package/src/components/waka-loot-box/index.tsx +1091 -0
  205. package/src/components/waka-magic-link/index.tsx +321 -0
  206. package/src/components/waka-magnetic-button/index.tsx +567 -0
  207. package/src/components/waka-mention-input/index.tsx +953 -0
  208. package/src/components/waka-metric-sparkline/index.tsx +627 -0
  209. package/src/components/waka-milestone-road/index.tsx +1064 -0
  210. package/src/components/waka-modal/index.tsx +374 -0
  211. package/src/components/waka-morph-button/index.tsx +495 -0
  212. package/src/components/waka-network-topology/index.tsx +801 -0
  213. package/src/components/waka-notifications/index.tsx +414 -0
  214. package/src/components/waka-number-input/index.tsx +373 -0
  215. package/src/components/waka-orbital-menu/index.tsx +445 -0
  216. package/src/components/waka-order-tracker/index.tsx +1041 -0
  217. package/src/components/waka-pagination/index.tsx +393 -0
  218. package/src/components/waka-password-strength/index.tsx +824 -0
  219. package/src/components/waka-payment-method-picker/index.tsx +715 -0
  220. package/src/components/waka-permission-matrix/index.tsx +1302 -0
  221. package/src/components/waka-phone-input/index.tsx +801 -0
  222. package/src/components/waka-pipeline-view/index.tsx +604 -0
  223. package/src/components/waka-player-card/index.tsx +691 -0
  224. package/src/components/waka-points-popup/index.tsx +366 -0
  225. package/src/components/waka-power-up/index.tsx +1155 -0
  226. package/src/components/waka-presence-indicator/index.tsx +1181 -0
  227. package/src/components/waka-pricing-table/index.tsx +755 -0
  228. package/src/components/waka-product-card/index.tsx +786 -0
  229. package/src/components/waka-progress-onboarding/index.tsx +878 -0
  230. package/src/components/waka-pull-to-refresh/index.tsx +451 -0
  231. package/src/components/waka-qrcode/index.tsx +232 -0
  232. package/src/components/waka-quest-card/index.tsx +1275 -0
  233. package/src/components/waka-quota-bar/index.tsx +693 -0
  234. package/src/components/waka-radar-score/index.tsx +512 -0
  235. package/src/components/waka-rank-badge/index.tsx +813 -0
  236. package/src/components/waka-rating-input/index.tsx +560 -0
  237. package/src/components/waka-reaction-picker/index.tsx +1062 -0
  238. package/src/components/waka-region-map/index.tsx +730 -0
  239. package/src/components/waka-resource-gauge/index.tsx +654 -0
  240. package/src/components/waka-resource-pool/index.tsx +1035 -0
  241. package/src/components/waka-rich-text-editor/index.tsx +594 -0
  242. package/src/components/waka-rollback-slider/index.tsx +891 -0
  243. package/src/components/waka-sankey-diagram/index.tsx +1032 -0
  244. package/src/components/waka-schedule-picker/index.tsx +1060 -0
  245. package/src/components/waka-scratch-card/index.tsx +914 -0
  246. package/src/components/waka-season-pass/index.tsx +886 -0
  247. package/src/components/waka-security-score/index.tsx +1126 -0
  248. package/src/components/waka-segmented-control/index.tsx +238 -0
  249. package/src/components/waka-server-rack/index.tsx +764 -0
  250. package/src/components/waka-session-manager/index.tsx +815 -0
  251. package/src/components/waka-signature-pad/index.tsx +744 -0
  252. package/src/components/waka-skeleton-wave/index.tsx +454 -0
  253. package/src/components/waka-skill-tree/index.tsx +1031 -0
  254. package/src/components/waka-sla-tracker/index.tsx +798 -0
  255. package/src/components/waka-slider-range/index.tsx +765 -0
  256. package/src/components/waka-spin-wheel/index.tsx +671 -0
  257. package/src/components/waka-spinner/index.tsx +284 -0
  258. package/src/components/waka-spotlight/index.tsx +410 -0
  259. package/src/components/waka-stat/index.tsx +428 -0
  260. package/src/components/waka-stats-hexagon/index.tsx +824 -0
  261. package/src/components/waka-status-matrix/index.tsx +565 -0
  262. package/src/components/waka-stepper/index.tsx +489 -0
  263. package/src/components/waka-streak-counter/index.tsx +334 -0
  264. package/src/components/waka-success-explosion/index.tsx +453 -0
  265. package/src/components/waka-swipe-card/index.tsx +574 -0
  266. package/src/components/waka-tabs-morph/index.tsx +509 -0
  267. package/src/components/waka-tag-input/index.tsx +877 -0
  268. package/src/components/waka-team-banner/index.tsx +1183 -0
  269. package/src/components/waka-terminal-output/index.tsx +836 -0
  270. package/src/components/waka-theme-creator/index.tsx +762 -0
  271. package/src/components/waka-theme-manager/index.tsx +654 -0
  272. package/src/components/waka-thread-view/index.tsx +874 -0
  273. package/src/components/waka-tilt-card/index.tsx +250 -0
  274. package/src/components/waka-time-picker/index.tsx +479 -0
  275. package/src/components/waka-timeline/index.tsx +385 -0
  276. package/src/components/waka-tooltip-tour/index.tsx +855 -0
  277. package/src/components/waka-tour-guide/index.tsx +920 -0
  278. package/src/components/waka-tournament-bracket/index.tsx +1276 -0
  279. package/src/components/waka-tree/index.tsx +557 -0
  280. package/src/components/waka-treemap-chart/index.tsx +1031 -0
  281. package/src/components/waka-two-factor-setup/index.tsx +995 -0
  282. package/src/components/waka-typewriter/index.tsx +566 -0
  283. package/src/components/waka-typing-indicator/index.tsx +649 -0
  284. package/src/components/waka-versus-card/index.tsx +1026 -0
  285. package/src/components/waka-video/index.tsx +557 -0
  286. package/src/components/waka-video-call/index.tsx +1087 -0
  287. package/src/components/waka-virtual-list/index.tsx +327 -0
  288. package/src/components/waka-voice-message/index.tsx +1019 -0
  289. package/src/components/waka-welcome-modal/index.tsx +790 -0
  290. package/src/components/waka-xp-bar/index.tsx +799 -0
@@ -0,0 +1,746 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils"
5
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../../components/card"
6
+ import { Button } from "../../components/button"
7
+ import { Input } from "../../components/input"
8
+ import { Label } from "../../components/label"
9
+ import { Textarea } from "../../components/textarea"
10
+ import { Avatar, AvatarFallback, AvatarImage } from "../../components/avatar"
11
+ import { Badge } from "../../components/badge"
12
+ import { Separator } from "../../components/separator"
13
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../components/tabs"
14
+ import {
15
+ Select,
16
+ SelectContent,
17
+ SelectItem,
18
+ SelectTrigger,
19
+ SelectValue,
20
+ } from "../../components/select"
21
+ import {
22
+ User,
23
+ Mail,
24
+ Phone,
25
+ MapPin,
26
+ Globe,
27
+ Calendar,
28
+ Building,
29
+ Briefcase,
30
+ Camera,
31
+ Edit3,
32
+ Save,
33
+ X,
34
+ Loader2,
35
+ Link as LinkIcon,
36
+ Twitter,
37
+ Github,
38
+ Linkedin,
39
+ } from "lucide-react"
40
+
41
+ // ============================================
42
+ // TYPES
43
+ // ============================================
44
+
45
+ export interface ProfileSocialLink {
46
+ id: string
47
+ type: "website" | "twitter" | "github" | "linkedin" | "other"
48
+ url: string
49
+ label?: string
50
+ }
51
+
52
+ export interface ProfileField {
53
+ id: string
54
+ label: string
55
+ value: string
56
+ type?: "text" | "email" | "tel" | "url" | "textarea" | "date" | "select"
57
+ placeholder?: string
58
+ options?: { value: string; label: string }[]
59
+ required?: boolean
60
+ disabled?: boolean
61
+ icon?: React.ReactNode
62
+ }
63
+
64
+ export interface ProfileSection {
65
+ id: string
66
+ title: string
67
+ description?: string
68
+ icon?: React.ReactNode
69
+ fields: ProfileField[]
70
+ }
71
+
72
+ export interface ProfileData {
73
+ /** Nom d'affichage */
74
+ displayName: string
75
+ /** Prénom */
76
+ firstName?: string
77
+ /** Nom */
78
+ lastName?: string
79
+ /** Email */
80
+ email: string
81
+ /** Téléphone */
82
+ phone?: string
83
+ /** Bio */
84
+ bio?: string
85
+ /** Avatar URL */
86
+ avatarUrl?: string
87
+ /** Rôle/Titre */
88
+ role?: string
89
+ /** Département */
90
+ department?: string
91
+ /** Entreprise */
92
+ company?: string
93
+ /** Localisation */
94
+ location?: string
95
+ /** Site web */
96
+ website?: string
97
+ /** Date d'inscription */
98
+ joinedAt?: Date | string
99
+ /** Liens sociaux */
100
+ socialLinks?: ProfileSocialLink[]
101
+ /** Champs personnalisés */
102
+ customFields?: Record<string, string>
103
+ }
104
+
105
+ export interface WakaProfileProps {
106
+ /** Données du profil */
107
+ profile: ProfileData
108
+ /** Mode édition */
109
+ editable?: boolean
110
+ /** En mode édition actuellement */
111
+ isEditing?: boolean
112
+ /** Callback pour activer/désactiver l'édition */
113
+ onEditToggle?: (editing: boolean) => void
114
+ /** Callback lors d'un changement */
115
+ onChange?: (field: string, value: string) => void
116
+ /** Callback de sauvegarde */
117
+ onSave?: (profile: ProfileData) => void | Promise<void>
118
+ /** En cours de sauvegarde */
119
+ saving?: boolean
120
+ /** Callback changement d'avatar */
121
+ onAvatarChange?: (file: File) => void | Promise<void>
122
+ /** Sections personnalisées */
123
+ customSections?: ProfileSection[]
124
+ /** Afficher les liens sociaux */
125
+ showSocialLinks?: boolean
126
+ /** Afficher les statistiques */
127
+ showStats?: boolean
128
+ /** Statistiques */
129
+ stats?: { label: string; value: string | number }[]
130
+ /** Layout */
131
+ layout?: "card" | "page" | "compact"
132
+ /** Tabs pour page layout */
133
+ useTabs?: boolean
134
+ /** Classes CSS additionnelles */
135
+ className?: string
136
+ }
137
+
138
+ // ============================================
139
+ // SUB-COMPONENTS
140
+ // ============================================
141
+
142
+ interface ProfileAvatarProps {
143
+ url?: string
144
+ name: string
145
+ size?: "sm" | "md" | "lg" | "xl"
146
+ editable?: boolean
147
+ onAvatarChange?: (file: File) => void
148
+ }
149
+
150
+ function ProfileAvatar({
151
+ url,
152
+ name,
153
+ size = "lg",
154
+ editable,
155
+ onAvatarChange,
156
+ }: ProfileAvatarProps) {
157
+ const fileInputRef = React.useRef<HTMLInputElement>(null)
158
+
159
+ const sizeClasses = {
160
+ sm: "h-12 w-12",
161
+ md: "h-16 w-16",
162
+ lg: "h-24 w-24",
163
+ xl: "h-32 w-32",
164
+ }
165
+
166
+ const initials = name
167
+ .split(" ")
168
+ .map((n) => n[0])
169
+ .join("")
170
+ .toUpperCase()
171
+ .slice(0, 2)
172
+
173
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
174
+ const file = e.target.files?.[0]
175
+ if (file && onAvatarChange) {
176
+ onAvatarChange(file)
177
+ }
178
+ }
179
+
180
+ return (
181
+ <div className="relative inline-block">
182
+ <Avatar className={cn(sizeClasses[size], "border-4 border-background shadow-lg")}>
183
+ <AvatarImage src={url} alt={name} />
184
+ <AvatarFallback className="text-lg font-medium">{initials}</AvatarFallback>
185
+ </Avatar>
186
+ {editable && onAvatarChange && (
187
+ <>
188
+ <input
189
+ ref={fileInputRef}
190
+ type="file"
191
+ accept="image/*"
192
+ onChange={handleFileChange}
193
+ className="hidden"
194
+ />
195
+ <Button
196
+ size="icon"
197
+ variant="secondary"
198
+ className="absolute bottom-0 right-0 h-8 w-8 rounded-full shadow-md"
199
+ onClick={() => fileInputRef.current?.click()}
200
+ >
201
+ <Camera className="h-4 w-4" />
202
+ </Button>
203
+ </>
204
+ )}
205
+ </div>
206
+ )
207
+ }
208
+
209
+ interface ProfileFieldInputProps {
210
+ field: ProfileField
211
+ value: string
212
+ onChange: (value: string) => void
213
+ isEditing: boolean
214
+ }
215
+
216
+ function ProfileFieldInput({
217
+ field,
218
+ value,
219
+ onChange,
220
+ isEditing,
221
+ }: ProfileFieldInputProps) {
222
+ if (!isEditing) {
223
+ return (
224
+ <div className="flex items-center gap-2">
225
+ {field.icon && (
226
+ <span className="text-muted-foreground">{field.icon}</span>
227
+ )}
228
+ <span className={cn(!value && "text-muted-foreground italic")}>
229
+ {value || "Non renseigné"}
230
+ </span>
231
+ </div>
232
+ )
233
+ }
234
+
235
+ const baseProps = {
236
+ id: field.id,
237
+ value: value || "",
238
+ onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
239
+ onChange(e.target.value),
240
+ placeholder: field.placeholder,
241
+ disabled: field.disabled,
242
+ }
243
+
244
+ if (field.type === "textarea") {
245
+ return <Textarea {...baseProps} rows={3} />
246
+ }
247
+
248
+ if (field.type === "select" && field.options) {
249
+ return (
250
+ <Select value={value} onValueChange={onChange} disabled={field.disabled}>
251
+ <SelectTrigger>
252
+ <SelectValue placeholder={field.placeholder || "Sélectionner..."} />
253
+ </SelectTrigger>
254
+ <SelectContent>
255
+ {field.options.map((option) => (
256
+ <SelectItem key={option.value} value={option.value}>
257
+ {option.label}
258
+ </SelectItem>
259
+ ))}
260
+ </SelectContent>
261
+ </Select>
262
+ )
263
+ }
264
+
265
+ return <Input type={field.type || "text"} {...baseProps} />
266
+ }
267
+
268
+ interface ProfileSectionCardProps {
269
+ section: ProfileSection
270
+ values: Record<string, string>
271
+ onChange: (fieldId: string, value: string) => void
272
+ isEditing: boolean
273
+ }
274
+
275
+ function ProfileSectionCard({
276
+ section,
277
+ values,
278
+ onChange,
279
+ isEditing,
280
+ }: ProfileSectionCardProps) {
281
+ return (
282
+ <Card>
283
+ <CardHeader>
284
+ <div className="flex items-center gap-2">
285
+ {section.icon && (
286
+ <span className="text-muted-foreground">{section.icon}</span>
287
+ )}
288
+ <div>
289
+ <CardTitle className="text-base">{section.title}</CardTitle>
290
+ {section.description && (
291
+ <CardDescription>{section.description}</CardDescription>
292
+ )}
293
+ </div>
294
+ </div>
295
+ </CardHeader>
296
+ <CardContent className="space-y-4">
297
+ {section.fields.map((field, index) => (
298
+ <React.Fragment key={field.id}>
299
+ <div className="space-y-2">
300
+ <Label htmlFor={field.id} className="text-sm font-medium">
301
+ {field.label}
302
+ {field.required && <span className="text-destructive ml-1">*</span>}
303
+ </Label>
304
+ <ProfileFieldInput
305
+ field={field}
306
+ value={values[field.id] || ""}
307
+ onChange={(value) => onChange(field.id, value)}
308
+ isEditing={isEditing}
309
+ />
310
+ </div>
311
+ {index < section.fields.length - 1 && isEditing && (
312
+ <Separator className="my-4" />
313
+ )}
314
+ </React.Fragment>
315
+ ))}
316
+ </CardContent>
317
+ </Card>
318
+ )
319
+ }
320
+
321
+ interface SocialLinksProps {
322
+ links: ProfileSocialLink[]
323
+ isEditing: boolean
324
+ onChange?: (links: ProfileSocialLink[]) => void
325
+ }
326
+
327
+ function SocialLinks({ links, isEditing }: SocialLinksProps) {
328
+ const getIcon = (type: ProfileSocialLink["type"]) => {
329
+ switch (type) {
330
+ case "twitter":
331
+ return <Twitter className="h-4 w-4" />
332
+ case "github":
333
+ return <Github className="h-4 w-4" />
334
+ case "linkedin":
335
+ return <Linkedin className="h-4 w-4" />
336
+ case "website":
337
+ return <Globe className="h-4 w-4" />
338
+ default:
339
+ return <LinkIcon className="h-4 w-4" />
340
+ }
341
+ }
342
+
343
+ if (!isEditing) {
344
+ return (
345
+ <div className="flex gap-2 flex-wrap">
346
+ {links.map((link) => (
347
+ <a
348
+ key={link.id}
349
+ href={link.url}
350
+ target="_blank"
351
+ rel="noopener noreferrer"
352
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-muted rounded-full text-sm hover:bg-muted/80 transition-colors"
353
+ >
354
+ {getIcon(link.type)}
355
+ <span>{link.label || link.type}</span>
356
+ </a>
357
+ ))}
358
+ </div>
359
+ )
360
+ }
361
+
362
+ return (
363
+ <div className="space-y-2">
364
+ {links.map((link) => (
365
+ <div key={link.id} className="flex items-center gap-2">
366
+ {getIcon(link.type)}
367
+ <Input value={link.url} className="flex-1" readOnly />
368
+ </div>
369
+ ))}
370
+ </div>
371
+ )
372
+ }
373
+
374
+ // ============================================
375
+ // MAIN COMPONENT
376
+ // ============================================
377
+
378
+ export function WakaProfile({
379
+ profile,
380
+ editable = false,
381
+ isEditing: externalIsEditing,
382
+ onEditToggle,
383
+ onChange,
384
+ onSave,
385
+ saving = false,
386
+ onAvatarChange,
387
+ customSections,
388
+ showSocialLinks = true,
389
+ showStats = false,
390
+ stats,
391
+ layout = "card",
392
+ useTabs = false,
393
+ className,
394
+ }: WakaProfileProps) {
395
+ const [internalIsEditing, setInternalIsEditing] = React.useState(false)
396
+ const [localProfile, setLocalProfile] = React.useState(profile)
397
+
398
+ const isEditing = externalIsEditing ?? internalIsEditing
399
+
400
+ React.useEffect(() => {
401
+ setLocalProfile(profile)
402
+ }, [profile])
403
+
404
+ const handleEditToggle = (editing: boolean) => {
405
+ if (onEditToggle) {
406
+ onEditToggle(editing)
407
+ } else {
408
+ setInternalIsEditing(editing)
409
+ }
410
+ if (!editing) {
411
+ setLocalProfile(profile) // Reset on cancel
412
+ }
413
+ }
414
+
415
+ const handleChange = (field: string, value: string) => {
416
+ setLocalProfile((prev) => ({ ...prev, [field]: value }))
417
+ onChange?.(field, value)
418
+ }
419
+
420
+ const handleSave = async () => {
421
+ await onSave?.(localProfile)
422
+ handleEditToggle(false)
423
+ }
424
+
425
+ // Default sections
426
+ const defaultSections: ProfileSection[] = [
427
+ {
428
+ id: "personal",
429
+ title: "Informations personnelles",
430
+ icon: <User className="h-5 w-5" />,
431
+ fields: [
432
+ {
433
+ id: "firstName",
434
+ label: "Prénom",
435
+ value: localProfile.firstName || "",
436
+ type: "text",
437
+ placeholder: "Jean",
438
+ icon: <User className="h-4 w-4" />,
439
+ },
440
+ {
441
+ id: "lastName",
442
+ label: "Nom",
443
+ value: localProfile.lastName || "",
444
+ type: "text",
445
+ placeholder: "Dupont",
446
+ },
447
+ {
448
+ id: "email",
449
+ label: "Email",
450
+ value: localProfile.email,
451
+ type: "email",
452
+ placeholder: "jean@exemple.com",
453
+ required: true,
454
+ icon: <Mail className="h-4 w-4" />,
455
+ },
456
+ {
457
+ id: "phone",
458
+ label: "Téléphone",
459
+ value: localProfile.phone || "",
460
+ type: "tel",
461
+ placeholder: "+33 6 12 34 56 78",
462
+ icon: <Phone className="h-4 w-4" />,
463
+ },
464
+ {
465
+ id: "bio",
466
+ label: "Bio",
467
+ value: localProfile.bio || "",
468
+ type: "textarea",
469
+ placeholder: "Parlez-nous de vous...",
470
+ },
471
+ ],
472
+ },
473
+ {
474
+ id: "professional",
475
+ title: "Informations professionnelles",
476
+ icon: <Briefcase className="h-5 w-5" />,
477
+ fields: [
478
+ {
479
+ id: "role",
480
+ label: "Poste",
481
+ value: localProfile.role || "",
482
+ type: "text",
483
+ placeholder: "Développeur Senior",
484
+ icon: <Briefcase className="h-4 w-4" />,
485
+ },
486
+ {
487
+ id: "department",
488
+ label: "Département",
489
+ value: localProfile.department || "",
490
+ type: "text",
491
+ placeholder: "Engineering",
492
+ },
493
+ {
494
+ id: "company",
495
+ label: "Entreprise",
496
+ value: localProfile.company || "",
497
+ type: "text",
498
+ placeholder: "Acme Inc.",
499
+ icon: <Building className="h-4 w-4" />,
500
+ },
501
+ {
502
+ id: "location",
503
+ label: "Localisation",
504
+ value: localProfile.location || "",
505
+ type: "text",
506
+ placeholder: "Paris, France",
507
+ icon: <MapPin className="h-4 w-4" />,
508
+ },
509
+ {
510
+ id: "website",
511
+ label: "Site web",
512
+ value: localProfile.website || "",
513
+ type: "url",
514
+ placeholder: "https://exemple.com",
515
+ icon: <Globe className="h-4 w-4" />,
516
+ },
517
+ ],
518
+ },
519
+ ]
520
+
521
+ const allSections = customSections || defaultSections
522
+
523
+ // Create values map from localProfile
524
+ const values: Record<string, string> = {
525
+ firstName: localProfile.firstName || "",
526
+ lastName: localProfile.lastName || "",
527
+ email: localProfile.email,
528
+ phone: localProfile.phone || "",
529
+ bio: localProfile.bio || "",
530
+ role: localProfile.role || "",
531
+ department: localProfile.department || "",
532
+ company: localProfile.company || "",
533
+ location: localProfile.location || "",
534
+ website: localProfile.website || "",
535
+ ...localProfile.customFields,
536
+ }
537
+
538
+ // Compact layout
539
+ if (layout === "compact") {
540
+ return (
541
+ <div className={cn("flex items-center gap-4", className)}>
542
+ <ProfileAvatar
543
+ url={localProfile.avatarUrl}
544
+ name={localProfile.displayName}
545
+ size="md"
546
+ />
547
+ <div>
548
+ <h3 className="font-semibold">{localProfile.displayName}</h3>
549
+ {localProfile.role && (
550
+ <p className="text-sm text-muted-foreground">{localProfile.role}</p>
551
+ )}
552
+ {localProfile.email && (
553
+ <p className="text-sm text-muted-foreground">{localProfile.email}</p>
554
+ )}
555
+ </div>
556
+ </div>
557
+ )
558
+ }
559
+
560
+ const renderHeader = () => (
561
+ <div className="flex flex-col items-center text-center gap-4 md:flex-row md:text-left">
562
+ <ProfileAvatar
563
+ url={localProfile.avatarUrl}
564
+ name={localProfile.displayName}
565
+ size="xl"
566
+ editable={editable && isEditing}
567
+ onAvatarChange={onAvatarChange}
568
+ />
569
+ <div className="flex-1">
570
+ <div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
571
+ <div>
572
+ <h1 className="text-2xl font-bold">{localProfile.displayName}</h1>
573
+ {localProfile.role && (
574
+ <p className="text-muted-foreground">{localProfile.role}</p>
575
+ )}
576
+ {localProfile.company && (
577
+ <p className="text-sm text-muted-foreground flex items-center gap-1">
578
+ <Building className="h-3 w-3" />
579
+ {localProfile.company}
580
+ </p>
581
+ )}
582
+ {localProfile.location && (
583
+ <p className="text-sm text-muted-foreground flex items-center gap-1">
584
+ <MapPin className="h-3 w-3" />
585
+ {localProfile.location}
586
+ </p>
587
+ )}
588
+ </div>
589
+ {editable && (
590
+ <div className="flex gap-2">
591
+ {isEditing ? (
592
+ <>
593
+ <Button
594
+ variant="outline"
595
+ size="sm"
596
+ onClick={() => handleEditToggle(false)}
597
+ disabled={saving}
598
+ >
599
+ <X className="mr-2 h-4 w-4" />
600
+ Annuler
601
+ </Button>
602
+ <Button size="sm" onClick={handleSave} disabled={saving}>
603
+ {saving ? (
604
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
605
+ ) : (
606
+ <Save className="mr-2 h-4 w-4" />
607
+ )}
608
+ Enregistrer
609
+ </Button>
610
+ </>
611
+ ) : (
612
+ <Button
613
+ variant="outline"
614
+ size="sm"
615
+ onClick={() => handleEditToggle(true)}
616
+ >
617
+ <Edit3 className="mr-2 h-4 w-4" />
618
+ Modifier
619
+ </Button>
620
+ )}
621
+ </div>
622
+ )}
623
+ </div>
624
+
625
+ {/* Stats */}
626
+ {showStats && stats && stats.length > 0 && (
627
+ <div className="flex gap-6 mt-4">
628
+ {stats.map((stat, index) => (
629
+ <div key={index} className="text-center">
630
+ <div className="text-2xl font-bold">{stat.value}</div>
631
+ <div className="text-xs text-muted-foreground">{stat.label}</div>
632
+ </div>
633
+ ))}
634
+ </div>
635
+ )}
636
+
637
+ {/* Social Links */}
638
+ {showSocialLinks && localProfile.socialLinks && localProfile.socialLinks.length > 0 && (
639
+ <div className="mt-4">
640
+ <SocialLinks links={localProfile.socialLinks} isEditing={isEditing} />
641
+ </div>
642
+ )}
643
+
644
+ {/* Join date */}
645
+ {localProfile.joinedAt && (
646
+ <p className="text-xs text-muted-foreground mt-4 flex items-center gap-1">
647
+ <Calendar className="h-3 w-3" />
648
+ Membre depuis{" "}
649
+ {new Date(localProfile.joinedAt).toLocaleDateString("fr-FR", {
650
+ month: "long",
651
+ year: "numeric",
652
+ })}
653
+ </p>
654
+ )}
655
+ </div>
656
+ </div>
657
+ )
658
+
659
+ const renderSections = () => (
660
+ <div className="space-y-6">
661
+ {allSections.map((section) => (
662
+ <ProfileSectionCard
663
+ key={section.id}
664
+ section={section}
665
+ values={values}
666
+ onChange={handleChange}
667
+ isEditing={isEditing}
668
+ />
669
+ ))}
670
+ </div>
671
+ )
672
+
673
+ // Card layout
674
+ if (layout === "card") {
675
+ return (
676
+ <Card className={className}>
677
+ <CardContent className="pt-6">
678
+ <div className="space-y-6">
679
+ {renderHeader()}
680
+ <Separator />
681
+ {renderSections()}
682
+ </div>
683
+ </CardContent>
684
+ </Card>
685
+ )
686
+ }
687
+
688
+ // Page layout
689
+ return (
690
+ <div className={cn("space-y-6", className)}>
691
+ {renderHeader()}
692
+
693
+ {useTabs ? (
694
+ <Tabs defaultValue={allSections[0]?.id} className="space-y-6">
695
+ <TabsList>
696
+ {allSections.map((section) => (
697
+ <TabsTrigger key={section.id} value={section.id} className="gap-2">
698
+ {section.icon}
699
+ {section.title}
700
+ </TabsTrigger>
701
+ ))}
702
+ </TabsList>
703
+ {allSections.map((section) => (
704
+ <TabsContent key={section.id} value={section.id}>
705
+ <ProfileSectionCard
706
+ section={section}
707
+ values={values}
708
+ onChange={handleChange}
709
+ isEditing={isEditing}
710
+ />
711
+ </TabsContent>
712
+ ))}
713
+ </Tabs>
714
+ ) : (
715
+ renderSections()
716
+ )}
717
+ </div>
718
+ )
719
+ }
720
+
721
+ // ============================================
722
+ // PRESETS
723
+ // ============================================
724
+
725
+ export const defaultProfileData: ProfileData = {
726
+ displayName: "Jean Dupont",
727
+ firstName: "Jean",
728
+ lastName: "Dupont",
729
+ email: "jean.dupont@exemple.com",
730
+ phone: "+33 6 12 34 56 78",
731
+ bio: "Développeur passionné avec plus de 10 ans d'expérience dans le développement web et mobile.",
732
+ avatarUrl: "",
733
+ role: "Lead Developer",
734
+ department: "Engineering",
735
+ company: "Acme Inc.",
736
+ location: "Paris, France",
737
+ website: "https://jeandupont.dev",
738
+ joinedAt: new Date("2023-01-15"),
739
+ socialLinks: [
740
+ { id: "1", type: "twitter", url: "https://twitter.com/jeandupont", label: "@jeandupont" },
741
+ { id: "2", type: "github", url: "https://github.com/jeandupont", label: "jeandupont" },
742
+ { id: "3", type: "linkedin", url: "https://linkedin.com/in/jeandupont", label: "Jean Dupont" },
743
+ ],
744
+ }
745
+
746
+ export default WakaProfile