@wakastellar/ui 2.4.0 → 3.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 (173) hide show
  1. package/dist/blocks/antivirus-dashboard/index.d.ts +44 -0
  2. package/dist/blocks/clamav-service-status/index.d.ts +35 -0
  3. package/dist/blocks/file-scan-uploader/index.d.ts +29 -0
  4. package/dist/blocks/index.d.ts +18 -9
  5. package/dist/blocks/quarantine-manager/index.d.ts +27 -0
  6. package/dist/blocks/scan-history-log/index.d.ts +28 -0
  7. package/dist/blocks/scan-policy-editor/index.d.ts +27 -0
  8. package/dist/blocks/scan-report-generator/index.d.ts +47 -0
  9. package/dist/blocks/signature-database-manager/index.d.ts +39 -0
  10. package/dist/blocks/threat-alert-banner/index.d.ts +26 -0
  11. package/dist/components/index.d.ts +4 -4
  12. package/dist/components/waka-signature-pad/index.d.ts +1 -1
  13. package/dist/exceljs.min-BcLLX0PC.js +29 -0
  14. package/dist/exceljs.min-KOayaaQ4.mjs +23013 -0
  15. package/dist/export.cjs.js +1 -1
  16. package/dist/export.d.ts +2 -2
  17. package/dist/export.es.js +1 -1
  18. package/dist/index.cjs.js +136 -136
  19. package/dist/index.es.js +29978 -27215
  20. package/dist/stories/Button.stories.d.ts +1 -1
  21. package/dist/stories/Header.stories.d.ts +1 -1
  22. package/dist/stories/Page.stories.d.ts +1 -1
  23. package/dist/useDataTableImport-COVnvslz.js +9 -0
  24. package/dist/useDataTableImport-DAlxBY8w.mjs +237 -0
  25. package/dist/utils/index.d.ts +1 -0
  26. package/dist/utils/logger.d.ts +9 -0
  27. package/package.json +6 -5
  28. package/src/blocks/antivirus-dashboard/AntivirusDashboard.stories.tsx +291 -0
  29. package/src/blocks/antivirus-dashboard/index.tsx +525 -0
  30. package/src/blocks/clamav-service-status/ClamAVServiceStatus.stories.tsx +195 -0
  31. package/src/blocks/clamav-service-status/index.tsx +370 -0
  32. package/src/blocks/file-scan-uploader/FileScanUploader.stories.tsx +257 -0
  33. package/src/blocks/file-scan-uploader/index.tsx +311 -0
  34. package/src/blocks/index.ts +163 -11
  35. package/src/blocks/quarantine-manager/QuarantineManager.stories.tsx +209 -0
  36. package/src/blocks/quarantine-manager/index.tsx +435 -0
  37. package/src/blocks/scan-history-log/ScanHistoryLog.stories.tsx +231 -0
  38. package/src/blocks/scan-history-log/index.tsx +406 -0
  39. package/src/blocks/scan-policy-editor/ScanPolicyEditor.stories.tsx +106 -0
  40. package/src/blocks/scan-policy-editor/index.tsx +418 -0
  41. package/src/blocks/scan-report-generator/ScanReportGenerator.stories.tsx +232 -0
  42. package/src/blocks/scan-report-generator/index.tsx +612 -0
  43. package/src/blocks/sidebar/index.tsx +2 -1
  44. package/src/blocks/signature-database-manager/SignatureDatabaseManager.stories.tsx +279 -0
  45. package/src/blocks/signature-database-manager/index.tsx +470 -0
  46. package/src/blocks/theme-creator-block/index.tsx +16 -2
  47. package/src/blocks/threat-alert-banner/ThreatAlertBanner.stories.tsx +152 -0
  48. package/src/blocks/threat-alert-banner/index.tsx +320 -0
  49. package/src/components/DataTable/DataTable.stories.tsx +203 -0
  50. package/src/components/DataTable/hooks/useDataTableExport.ts +38 -31
  51. package/src/components/DataTable/hooks/useDataTableImport.ts +31 -20
  52. package/src/components/error-boundary/ErrorBoundary.stories.tsx +125 -0
  53. package/src/components/index.ts +45 -4
  54. package/src/components/language-selector/LanguageSelector.stories.tsx +112 -0
  55. package/src/components/theme-selector/ThemeSelector.stories.tsx +77 -0
  56. package/src/components/toaster/Toaster.stories.tsx +67 -0
  57. package/src/components/waka-activity-feed/WakaActivityFeed.stories.tsx +116 -0
  58. package/src/components/waka-ad-banner/WakaAdBanner.stories.tsx +102 -0
  59. package/src/components/waka-ad-fallback/WakaAdFallback.stories.tsx +117 -0
  60. package/src/components/waka-ad-inline/WakaAdInline.stories.tsx +105 -0
  61. package/src/components/waka-ad-interstitial/WakaAdInterstitial.stories.tsx +92 -0
  62. package/src/components/waka-ad-placeholder/WakaAdPlaceholder.stories.tsx +89 -0
  63. package/src/components/waka-ad-provider/WakaAdProvider.stories.tsx +110 -0
  64. package/src/components/waka-ad-sidebar/WakaAdSidebar.stories.tsx +89 -0
  65. package/src/components/waka-ad-sidebar/index.tsx +3 -2
  66. package/src/components/waka-ad-sticky-footer/WakaAdStickyFooter.stories.tsx +88 -0
  67. package/src/components/waka-address-autocomplete/WakaAddressAutocomplete.stories.tsx +46 -0
  68. package/src/components/waka-admincrumb/WakaAdmincrumb.stories.tsx +166 -0
  69. package/src/components/waka-alert-panel/WakaAlertPanel.stories.tsx +45 -0
  70. package/src/components/waka-alert-stack/WakaAlertStack.stories.tsx +62 -0
  71. package/src/components/waka-allocation-matrix/WakaAllocationMatrix.stories.tsx +68 -0
  72. package/src/components/waka-approval-chain/WakaApprovalChain.stories.tsx +63 -0
  73. package/src/components/waka-audit-log/WakaAuditLog.stories.tsx +73 -0
  74. package/src/components/waka-autocomplete/WakaAutocomplete.stories.tsx +132 -172
  75. package/src/components/waka-biometric-prompt/WakaBiometricPrompt.stories.tsx +48 -0
  76. package/src/components/waka-breadcrumb/WakaBreadcrumb.stories.tsx +74 -191
  77. package/src/components/waka-breadcrumb-path/WakaBreadcrumbPath.stories.tsx +40 -0
  78. package/src/components/waka-budget-burn/WakaBudgetBurn.stories.tsx +86 -0
  79. package/src/components/waka-capacity-planner/WakaCapacityPlanner.stories.tsx +273 -0
  80. package/src/components/waka-cart-summary/WakaCartSummary.stories.tsx +176 -0
  81. package/src/components/waka-cart-summary/index.tsx +19 -10
  82. package/src/components/waka-challenge-timer/WakaChallengeTimer.stories.tsx +98 -0
  83. package/src/components/waka-chat-bubble/WakaChatBubble.stories.tsx +118 -0
  84. package/src/components/waka-checklist/WakaChecklist.stories.tsx +71 -0
  85. package/src/components/waka-checkout-stepper/WakaCheckoutStepper.stories.tsx +102 -0
  86. package/src/components/waka-cohort-table/WakaCohortTable.stories.tsx +56 -0
  87. package/src/components/waka-color-picker/WakaColorPicker.stories.tsx +99 -155
  88. package/src/components/waka-combo-counter/WakaComboCounter.stories.tsx +128 -0
  89. package/src/components/waka-command-bar/WakaCommandBar.stories.tsx +45 -0
  90. package/src/components/waka-compare-period/WakaComparePeriod.stories.tsx +76 -0
  91. package/src/components/waka-config-comparator/WakaConfigComparator.stories.tsx +143 -0
  92. package/src/components/waka-connection-matrix/WakaConnectionMatrix.stories.tsx +52 -0
  93. package/src/components/waka-content-recommendation/WakaContentRecommendation.stories.tsx +41 -0
  94. package/src/components/waka-coupon-input/WakaCouponInput.stories.tsx +126 -0
  95. package/src/components/waka-credit-card-input/WakaCreditCardInput.stories.tsx +120 -0
  96. package/src/components/waka-datetime-picker.form-integration/WakaDateTimePickerForm.stories.tsx +79 -0
  97. package/src/components/waka-dependency-tree/WakaDependencyTree.stories.tsx +72 -0
  98. package/src/components/waka-device-trust/WakaDeviceTrust.stories.tsx +109 -0
  99. package/src/components/waka-empty-state/WakaEmptyState.stories.tsx +87 -0
  100. package/src/components/waka-feature-announcement/WakaFeatureAnnouncement.stories.tsx +47 -0
  101. package/src/components/waka-feature-flag-row/WakaFeatureFlagRow.stories.tsx +188 -0
  102. package/src/components/waka-file-upload/WakaFileUpload.stories.tsx +118 -174
  103. package/src/components/waka-floating-nav/WakaFloatingNav.stories.tsx +53 -0
  104. package/src/components/waka-goal-progress/WakaGoalProgress.stories.tsx +137 -0
  105. package/src/components/waka-hotspot/WakaHotspot.stories.tsx +56 -0
  106. package/src/components/waka-invoice-preview/WakaInvoicePreview.stories.tsx +169 -0
  107. package/src/components/waka-kpi-dashboard/WakaKpiDashboard.stories.tsx +46 -0
  108. package/src/components/waka-level-progress/WakaLevelProgress.stories.tsx +94 -75
  109. package/src/components/waka-liquid-button/WakaLiquidButton.stories.tsx +45 -0
  110. package/src/components/waka-magic-link/WakaMagicLink.stories.tsx +61 -0
  111. package/src/components/waka-magnetic-button/WakaMagneticButton.stories.tsx +40 -0
  112. package/src/components/waka-mention-input/WakaMentionInput.stories.tsx +140 -0
  113. package/src/components/waka-milestone-road/WakaMilestoneRoad.stories.tsx +143 -0
  114. package/src/components/waka-orbital-menu/WakaOrbitalMenu.stories.tsx +54 -0
  115. package/src/components/waka-order-tracker/WakaOrderTracker.stories.tsx +163 -0
  116. package/src/components/waka-outstream-video/WakaOutstreamVideo.stories.tsx +94 -0
  117. package/src/components/waka-pagination/WakaPagination.stories.tsx +110 -280
  118. package/src/components/waka-password-strength/WakaPasswordStrength.stories.tsx +132 -268
  119. package/src/components/waka-payment-method-picker/WakaPaymentMethodPicker.stories.tsx +141 -0
  120. package/src/components/waka-permission-matrix/WakaPermissionMatrix.stories.tsx +124 -0
  121. package/src/components/waka-phone-input/WakaPhoneInput.stories.tsx +56 -0
  122. package/src/components/waka-points-popup/WakaPointsPopup.stories.tsx +96 -0
  123. package/src/components/waka-power-up/WakaPowerUp.stories.tsx +121 -0
  124. package/src/components/waka-presence-indicator/WakaPresenceIndicator.stories.tsx +49 -0
  125. package/src/components/waka-pricing-table/WakaPricingTable.stories.tsx +159 -0
  126. package/src/components/waka-product-card/WakaProductCard.stories.tsx +202 -0
  127. package/src/components/waka-progress-onboarding/WakaProgressOnboarding.stories.tsx +57 -0
  128. package/src/components/waka-pull-to-refresh/WakaPullToRefresh.stories.tsx +51 -0
  129. package/src/components/waka-rank-badge/WakaRankBadge.stories.tsx +108 -0
  130. package/src/components/waka-rating-input/WakaRatingInput.stories.tsx +51 -0
  131. package/src/components/waka-reaction-picker/WakaReactionPicker.stories.tsx +52 -0
  132. package/src/components/waka-region-map/WakaRegionMap.stories.tsx +181 -0
  133. package/src/components/waka-resource-pool/WakaResourcePool.stories.tsx +70 -0
  134. package/src/components/waka-rich-text-editor/WakaRichTextEditor.stories.tsx +108 -197
  135. package/src/components/waka-rollback-slider/WakaRollbackSlider.stories.tsx +41 -0
  136. package/src/components/waka-schedule-picker/WakaSchedulePicker.stories.tsx +64 -0
  137. package/src/components/waka-season-pass/WakaSeasonPass.stories.tsx +107 -0
  138. package/src/components/waka-security-scan-result/WakaSecurityScanResult.stories.tsx +146 -0
  139. package/src/components/waka-security-score/WakaSecurityScore.stories.tsx +63 -0
  140. package/src/components/waka-session-manager/WakaSessionManager.stories.tsx +68 -0
  141. package/src/components/waka-signature-pad/WakaSignaturePad.stories.tsx +159 -0
  142. package/src/components/waka-signature-pad/index.tsx +5 -3
  143. package/src/components/waka-sla-tracker/WakaSlaTracker.stories.tsx +65 -0
  144. package/src/components/waka-slider-range/WakaSliderRange.stories.tsx +66 -0
  145. package/src/components/waka-sponsored-badge/WakaSponsoredBadge.stories.tsx +60 -0
  146. package/src/components/waka-sponsored-card/WakaSponsoredCard.stories.tsx +64 -0
  147. package/src/components/waka-sponsored-feed/WakaSponsoredFeed.stories.tsx +58 -0
  148. package/src/components/waka-spotlight/WakaSpotlight.stories.tsx +53 -0
  149. package/src/components/waka-stats-hexagon/WakaStatsHexagon.stories.tsx +161 -0
  150. package/src/components/waka-stepper/WakaStepper.stories.tsx +137 -410
  151. package/src/components/waka-swipe-card/WakaSwipeCard.stories.tsx +51 -0
  152. package/src/components/waka-tag-input/WakaTagInput.stories.tsx +224 -0
  153. package/src/components/waka-team-banner/WakaTeamBanner.stories.tsx +50 -0
  154. package/src/components/waka-theme-creator/WakaThemeCreator.stories.tsx +58 -0
  155. package/src/components/waka-theme-manager/WakaThemeManager.stories.tsx +298 -0
  156. package/src/components/waka-theme-manager/index.tsx +6 -11
  157. package/src/components/waka-thread-view/WakaThreadView.stories.tsx +143 -0
  158. package/src/components/waka-timeline/WakaTimeline.stories.tsx +171 -324
  159. package/src/components/waka-tooltip-tour/WakaTooltipTour.stories.tsx +92 -0
  160. package/src/components/waka-tour-guide/WakaTourGuide.stories.tsx +89 -0
  161. package/src/components/waka-treemap-chart/WakaTreemapChart.stories.tsx +234 -129
  162. package/src/components/waka-treemap-chart/index.tsx +2 -2
  163. package/src/components/waka-two-factor-setup/WakaTwoFactorSetup.stories.tsx +142 -0
  164. package/src/components/waka-typing-indicator/WakaTypingIndicator.stories.tsx +134 -0
  165. package/src/components/waka-video-ad/WakaVideoAd.stories.tsx +138 -0
  166. package/src/components/waka-video-call/WakaVideoCall.stories.tsx +186 -0
  167. package/src/components/waka-video-overlay/WakaVideoOverlay.stories.tsx +100 -0
  168. package/src/components/waka-voice-message/WakaVoiceMessage.stories.tsx +190 -0
  169. package/src/components/waka-welcome-modal/WakaWelcomeModal.stories.tsx +87 -0
  170. package/src/components/waka-xp-bar/WakaXPBar.stories.tsx +29 -29
  171. package/dist/useDataTableImport-D8R2HQl6.mjs +0 -229
  172. package/dist/useDataTableImport-S_hhA5Wo.js +0 -9
  173. package/src/components/DataTable/README.md +0 -446
@@ -0,0 +1,257 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { FileScanUploader, type ScannedFile } from "./index"
4
+
5
+ const meta = {
6
+ title: "Blocks/Antivirus/FileScanUploader",
7
+ component: FileScanUploader,
8
+ parameters: {
9
+ layout: "centered",
10
+ },
11
+ tags: ["autodocs"],
12
+ args: {
13
+ onFilesAdded: (files: File[]) => console.log("Files added:", files),
14
+ onScanStart: (fileId: string) => console.log("Scan start:", fileId),
15
+ onScanComplete: (fileId: string, result: string, threatName?: string) => console.log("Scan complete:", fileId, result, threatName),
16
+ onFileRemove: (fileId: string) => console.log("File remove:", fileId),
17
+ onScanAll: () => console.log("Scan all"),
18
+ },
19
+ decorators: [
20
+ (Story: React.ComponentType) => (
21
+ <div className="w-[800px]">
22
+ <Story />
23
+ </div>
24
+ ),
25
+ ],
26
+ } satisfies Meta<typeof FileScanUploader>
27
+
28
+ export default meta
29
+ type Story = StoryObj<typeof meta>
30
+
31
+ export const Empty: Story = {
32
+ args: {
33
+ files: [],
34
+ },
35
+ }
36
+
37
+ export const WithFiles: Story = {
38
+ args: {
39
+ files: [
40
+ {
41
+ id: "1",
42
+ file: { name: "document.pdf", size: 2500000, type: "application/pdf" },
43
+ status: "pending",
44
+ progress: 0,
45
+ },
46
+ {
47
+ id: "2",
48
+ file: { name: "image.jpg", size: 1200000, type: "image/jpeg" },
49
+ status: "scanning",
50
+ progress: 45,
51
+ },
52
+ {
53
+ id: "3",
54
+ file: { name: "report.xlsx", size: 850000, type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" },
55
+ status: "clean",
56
+ progress: 100,
57
+ scanDuration: 1247,
58
+ scannedAt: new Date(),
59
+ },
60
+ {
61
+ id: "4",
62
+ file: { name: "suspicious.exe", size: 450000, type: "application/x-msdownload" },
63
+ status: "infected",
64
+ progress: 100,
65
+ threatName: "Trojan.GenericKD.46542395",
66
+ scanDuration: 892,
67
+ scannedAt: new Date(),
68
+ },
69
+ ],
70
+ },
71
+ }
72
+
73
+ export const AllClean: Story = {
74
+ args: {
75
+ files: [
76
+ {
77
+ id: "1",
78
+ file: { name: "document1.pdf", size: 2500000, type: "application/pdf" },
79
+ status: "clean",
80
+ progress: 100,
81
+ scanDuration: 1150,
82
+ scannedAt: new Date(),
83
+ },
84
+ {
85
+ id: "2",
86
+ file: { name: "document2.pdf", size: 1800000, type: "application/pdf" },
87
+ status: "clean",
88
+ progress: 100,
89
+ scanDuration: 980,
90
+ scannedAt: new Date(),
91
+ },
92
+ {
93
+ id: "3",
94
+ file: { name: "image.png", size: 950000, type: "image/png" },
95
+ status: "clean",
96
+ progress: 100,
97
+ scanDuration: 750,
98
+ scannedAt: new Date(),
99
+ },
100
+ ],
101
+ },
102
+ }
103
+
104
+ export const MultipleInfected: Story = {
105
+ args: {
106
+ files: [
107
+ {
108
+ id: "1",
109
+ file: { name: "malware1.exe", size: 650000, type: "application/x-msdownload" },
110
+ status: "infected",
111
+ progress: 100,
112
+ threatName: "Trojan.GenericKD.46542395",
113
+ scanDuration: 1200,
114
+ scannedAt: new Date(),
115
+ },
116
+ {
117
+ id: "2",
118
+ file: { name: "virus.zip", size: 3200000, type: "application/zip" },
119
+ status: "infected",
120
+ progress: 100,
121
+ threatName: "Win32.Worm.Generic",
122
+ scanDuration: 2100,
123
+ scannedAt: new Date(),
124
+ },
125
+ {
126
+ id: "3",
127
+ file: { name: "clean-document.pdf", size: 1200000, type: "application/pdf" },
128
+ status: "clean",
129
+ progress: 100,
130
+ scanDuration: 890,
131
+ scannedAt: new Date(),
132
+ },
133
+ ],
134
+ },
135
+ }
136
+
137
+ export const Scanning: Story = {
138
+ args: {
139
+ files: [
140
+ {
141
+ id: "1",
142
+ file: { name: "large-file.zip", size: 95000000, type: "application/zip" },
143
+ status: "scanning",
144
+ progress: 23,
145
+ },
146
+ {
147
+ id: "2",
148
+ file: { name: "video.mp4", size: 45000000, type: "video/mp4" },
149
+ status: "scanning",
150
+ progress: 67,
151
+ },
152
+ {
153
+ id: "3",
154
+ file: { name: "presentation.pptx", size: 8500000, type: "application/vnd.openxmlformats-officedocument.presentationml.presentation" },
155
+ status: "scanning",
156
+ progress: 89,
157
+ },
158
+ ],
159
+ },
160
+ }
161
+
162
+ export const WithErrors: Story = {
163
+ args: {
164
+ files: [
165
+ {
166
+ id: "1",
167
+ file: { name: "corrupted-file.dat", size: 2500000, type: "application/octet-stream" },
168
+ status: "error",
169
+ progress: 0,
170
+ },
171
+ {
172
+ id: "2",
173
+ file: { name: "timeout-file.bin", size: 150000000, type: "application/octet-stream" },
174
+ status: "error",
175
+ progress: 45,
176
+ },
177
+ {
178
+ id: "3",
179
+ file: { name: "valid-file.pdf", size: 1200000, type: "application/pdf" },
180
+ status: "clean",
181
+ progress: 100,
182
+ scanDuration: 1100,
183
+ scannedAt: new Date(),
184
+ },
185
+ ],
186
+ },
187
+ }
188
+
189
+ export const MixedStatuses: Story = {
190
+ args: {
191
+ files: [
192
+ {
193
+ id: "1",
194
+ file: { name: "waiting.pdf", size: 1500000, type: "application/pdf" },
195
+ status: "pending",
196
+ progress: 0,
197
+ },
198
+ {
199
+ id: "2",
200
+ file: { name: "processing.docx", size: 2200000, type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" },
201
+ status: "scanning",
202
+ progress: 52,
203
+ },
204
+ {
205
+ id: "3",
206
+ file: { name: "safe-image.jpg", size: 1800000, type: "image/jpeg" },
207
+ status: "clean",
208
+ progress: 100,
209
+ scanDuration: 980,
210
+ scannedAt: new Date(),
211
+ },
212
+ {
213
+ id: "4",
214
+ file: { name: "dangerous.scr", size: 350000, type: "application/x-msdownload" },
215
+ status: "infected",
216
+ progress: 100,
217
+ threatName: "Backdoor.Win32.Agent",
218
+ scanDuration: 750,
219
+ scannedAt: new Date(),
220
+ },
221
+ {
222
+ id: "5",
223
+ file: { name: "failed-scan.bin", size: 5000000, type: "application/octet-stream" },
224
+ status: "error",
225
+ progress: 15,
226
+ },
227
+ ],
228
+ },
229
+ }
230
+
231
+ export const LargeFiles: Story = {
232
+ args: {
233
+ files: [
234
+ {
235
+ id: "1",
236
+ file: { name: "large-video.mkv", size: 98500000, type: "video/x-matroska" },
237
+ status: "scanning",
238
+ progress: 12,
239
+ },
240
+ {
241
+ id: "2",
242
+ file: { name: "database-backup.sql", size: 87300000, type: "application/sql" },
243
+ status: "pending",
244
+ progress: 0,
245
+ },
246
+ ],
247
+ maxFileSize: 100 * 1024 * 1024, // 100MB
248
+ },
249
+ }
250
+
251
+ export const CustomMaxSize: Story = {
252
+ args: {
253
+ files: [],
254
+ maxFileSize: 50 * 1024 * 1024, // 50MB
255
+ acceptedTypes: [".pdf", ".docx", ".xlsx", ".jpg", ".png"],
256
+ },
257
+ }
@@ -0,0 +1,311 @@
1
+ "use client"
2
+
3
+ import { useState, useCallback, useRef } from "react"
4
+ import { Upload, X, Shield, ShieldAlert, CheckCircle2, AlertCircle, Scan } from "lucide-react"
5
+ import { Button } from "../../components/button"
6
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../../components/card"
7
+ import { Badge } from "../../components/badge"
8
+ import { Progress } from "../../components/progress"
9
+ import { cn } from "../../utils/cn"
10
+
11
+ export type FileScanStatus = "pending" | "scanning" | "clean" | "infected" | "error"
12
+
13
+ export interface ScannedFile {
14
+ id: string
15
+ file: { name: string; size: number; type: string }
16
+ status: FileScanStatus
17
+ progress: number
18
+ threatName?: string
19
+ scanDuration?: number
20
+ scannedAt?: Date
21
+ }
22
+
23
+ export interface FileScanUploaderProps {
24
+ files?: ScannedFile[]
25
+ maxFileSize?: number // bytes, default 100MB
26
+ acceptedTypes?: string[] // e.g. ["*/*"]
27
+ maxConcurrentScans?: number
28
+ onFilesAdded?: (files: File[]) => void
29
+ onScanStart?: (fileId: string) => void
30
+ onScanComplete?: (fileId: string, result: FileScanStatus, threatName?: string) => void
31
+ onFileRemove?: (fileId: string) => void
32
+ onScanAll?: () => void
33
+ className?: string
34
+ }
35
+
36
+ const formatFileSize = (bytes: number): string => {
37
+ if (bytes === 0) return "0 Bytes"
38
+ const k = 1024
39
+ const sizes = ["Bytes", "KB", "MB", "GB"]
40
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
41
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i]
42
+ }
43
+
44
+ const getStatusIcon = (status: FileScanStatus) => {
45
+ switch (status) {
46
+ case "pending":
47
+ return <Shield className="h-4 w-4 text-muted-foreground" />
48
+ case "scanning":
49
+ return <Scan className="h-4 w-4 text-blue-500 animate-pulse" />
50
+ case "clean":
51
+ return <CheckCircle2 className="h-4 w-4 text-green-500" />
52
+ case "infected":
53
+ return <ShieldAlert className="h-4 w-4 text-red-500" />
54
+ case "error":
55
+ return <AlertCircle className="h-4 w-4 text-orange-500" />
56
+ }
57
+ }
58
+
59
+ const getStatusBadge = (status: FileScanStatus) => {
60
+ switch (status) {
61
+ case "pending":
62
+ return <Badge variant="outline">En attente</Badge>
63
+ case "scanning":
64
+ return <Badge variant="outline" className="border-blue-500 text-blue-500">Scan en cours</Badge>
65
+ case "clean":
66
+ return <Badge variant="outline" className="border-green-500 text-green-500">Sain</Badge>
67
+ case "infected":
68
+ return <Badge variant="destructive">Infecté</Badge>
69
+ case "error":
70
+ return <Badge variant="outline" className="border-orange-500 text-orange-500">Erreur</Badge>
71
+ }
72
+ }
73
+
74
+ export function FileScanUploader({
75
+ files = [],
76
+ maxFileSize = 100 * 1024 * 1024, // 100MB default
77
+ acceptedTypes = ["*/*"],
78
+ maxConcurrentScans = 3,
79
+ onFilesAdded,
80
+ onScanStart,
81
+ onScanComplete,
82
+ onFileRemove,
83
+ onScanAll,
84
+ className,
85
+ }: FileScanUploaderProps) {
86
+ const [isDragging, setIsDragging] = useState(false)
87
+ const fileInputRef = useRef<HTMLInputElement>(null)
88
+
89
+ const handleDragOver = useCallback((e: React.DragEvent) => {
90
+ e.preventDefault()
91
+ e.stopPropagation()
92
+ setIsDragging(true)
93
+ }, [])
94
+
95
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
96
+ e.preventDefault()
97
+ e.stopPropagation()
98
+ setIsDragging(false)
99
+ }, [])
100
+
101
+ const handleDrop = useCallback(
102
+ (e: React.DragEvent) => {
103
+ e.preventDefault()
104
+ e.stopPropagation()
105
+ setIsDragging(false)
106
+
107
+ const droppedFiles = Array.from(e.dataTransfer.files)
108
+ if (onFilesAdded) {
109
+ onFilesAdded(droppedFiles)
110
+ }
111
+ },
112
+ [onFilesAdded]
113
+ )
114
+
115
+ const handleFileSelect = useCallback(
116
+ (e: React.ChangeEvent<HTMLInputElement>) => {
117
+ const selectedFiles = e.target.files ? Array.from(e.target.files) : []
118
+ if (onFilesAdded) {
119
+ onFilesAdded(selectedFiles)
120
+ }
121
+ },
122
+ [onFilesAdded]
123
+ )
124
+
125
+ const handleClick = () => {
126
+ fileInputRef.current?.click()
127
+ }
128
+
129
+ const pendingCount = files.filter((f) => f.status === "pending").length
130
+ const scanningCount = files.filter((f) => f.status === "scanning").length
131
+ const cleanCount = files.filter((f) => f.status === "clean").length
132
+ const infectedCount = files.filter((f) => f.status === "infected").length
133
+ const errorCount = files.filter((f) => f.status === "error").length
134
+
135
+ return (
136
+ <Card className={cn("w-full", className)}>
137
+ <CardHeader>
138
+ <div className="flex items-center justify-between">
139
+ <div>
140
+ <CardTitle>Scan Antivirus (ClamAV)</CardTitle>
141
+ <CardDescription>
142
+ Uploadez vos fichiers pour analyse de sécurité
143
+ </CardDescription>
144
+ </div>
145
+ {files.length > 0 && (
146
+ <div className="flex items-center gap-2">
147
+ <span className="text-sm text-muted-foreground">
148
+ {files.length} fichier{files.length > 1 ? "s" : ""}
149
+ </span>
150
+ {pendingCount > 0 && (
151
+ <Button
152
+ size="sm"
153
+ variant="outline"
154
+ onClick={onScanAll}
155
+ className="gap-2"
156
+ >
157
+ <Scan className="h-4 w-4" />
158
+ Scanner tout
159
+ </Button>
160
+ )}
161
+ <Button
162
+ size="sm"
163
+ variant="ghost"
164
+ onClick={() => files.forEach((f) => onFileRemove?.(f.id))}
165
+ >
166
+ Tout supprimer
167
+ </Button>
168
+ </div>
169
+ )}
170
+ </div>
171
+ </CardHeader>
172
+ <CardContent className="space-y-4">
173
+ {/* Drop zone */}
174
+ <div
175
+ onDragOver={handleDragOver}
176
+ onDragLeave={handleDragLeave}
177
+ onDrop={handleDrop}
178
+ onClick={handleClick}
179
+ className={cn(
180
+ "relative flex min-h-[200px] cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed p-8 transition-colors",
181
+ isDragging
182
+ ? "border-primary bg-primary/5"
183
+ : "border-muted-foreground/25 hover:border-primary/50 hover:bg-muted/50"
184
+ )}
185
+ >
186
+ <Upload className="h-10 w-10 text-muted-foreground" />
187
+ <div className="text-center">
188
+ <p className="text-sm font-medium">
189
+ Glissez vos fichiers ici ou cliquez pour parcourir
190
+ </p>
191
+ <p className="mt-1 text-xs text-muted-foreground">
192
+ Taille max: {formatFileSize(maxFileSize)}
193
+ </p>
194
+ </div>
195
+ <input
196
+ ref={fileInputRef}
197
+ type="file"
198
+ multiple
199
+ accept={acceptedTypes.join(",")}
200
+ onChange={handleFileSelect}
201
+ className="hidden"
202
+ />
203
+ </div>
204
+
205
+ {/* Stats badges */}
206
+ {files.length > 0 && (
207
+ <div className="flex flex-wrap gap-2">
208
+ {cleanCount > 0 && (
209
+ <Badge variant="outline" className="border-green-500 text-green-500">
210
+ <CheckCircle2 className="mr-1 h-3 w-3" />
211
+ {cleanCount} sain{cleanCount > 1 ? "s" : ""}
212
+ </Badge>
213
+ )}
214
+ {infectedCount > 0 && (
215
+ <Badge variant="destructive">
216
+ <ShieldAlert className="mr-1 h-3 w-3" />
217
+ {infectedCount} infecté{infectedCount > 1 ? "s" : ""}
218
+ </Badge>
219
+ )}
220
+ {scanningCount > 0 && (
221
+ <Badge variant="outline" className="border-blue-500 text-blue-500">
222
+ <Scan className="mr-1 h-3 w-3" />
223
+ {scanningCount} en cours
224
+ </Badge>
225
+ )}
226
+ {pendingCount > 0 && (
227
+ <Badge variant="outline">
228
+ <Shield className="mr-1 h-3 w-3" />
229
+ {pendingCount} en attente
230
+ </Badge>
231
+ )}
232
+ {errorCount > 0 && (
233
+ <Badge variant="outline" className="border-orange-500 text-orange-500">
234
+ <AlertCircle className="mr-1 h-3 w-3" />
235
+ {errorCount} erreur{errorCount > 1 ? "s" : ""}
236
+ </Badge>
237
+ )}
238
+ </div>
239
+ )}
240
+
241
+ {/* Files list */}
242
+ {files.length > 0 && (
243
+ <div className="space-y-2">
244
+ {files.map((scannedFile) => (
245
+ <div
246
+ key={scannedFile.id}
247
+ className={cn(
248
+ "rounded-lg border p-4 transition-colors",
249
+ scannedFile.status === "infected" && "border-red-500/50 bg-red-50/50 dark:bg-red-950/20",
250
+ scannedFile.status === "clean" && "border-green-500/50 bg-green-50/50 dark:bg-green-950/20"
251
+ )}
252
+ >
253
+ <div className="flex items-start justify-between gap-4">
254
+ <div className="flex flex-1 items-start gap-3">
255
+ <div className="mt-1">{getStatusIcon(scannedFile.status)}</div>
256
+ <div className="flex-1 space-y-1">
257
+ <div className="flex items-center gap-2">
258
+ <span className="font-medium text-sm">{scannedFile.file.name}</span>
259
+ {getStatusBadge(scannedFile.status)}
260
+ </div>
261
+ <p className="text-xs text-muted-foreground">
262
+ {formatFileSize(scannedFile.file.size)}
263
+ {scannedFile.scanDuration && (
264
+ <> • Scanné en {scannedFile.scanDuration}ms</>
265
+ )}
266
+ </p>
267
+ {scannedFile.status === "scanning" && (
268
+ <div className="mt-2">
269
+ <Progress value={scannedFile.progress} className="h-1.5" />
270
+ <p className="mt-1 text-xs text-muted-foreground">
271
+ Scan en cours... {scannedFile.progress}%
272
+ </p>
273
+ </div>
274
+ )}
275
+ {scannedFile.status === "infected" && scannedFile.threatName && (
276
+ <div className="mt-2 flex items-center gap-2 rounded-md bg-red-100 px-2 py-1 dark:bg-red-950/50">
277
+ <ShieldAlert className="h-3 w-3 text-red-600 dark:text-red-400" />
278
+ <span className="text-xs font-medium text-red-600 dark:text-red-400">
279
+ Menace détectée: {scannedFile.threatName}
280
+ </span>
281
+ </div>
282
+ )}
283
+ {scannedFile.status === "clean" && scannedFile.scannedAt && (
284
+ <div className="mt-2 flex items-center gap-2">
285
+ <CheckCircle2 className="h-3 w-3 text-green-600 dark:text-green-400" />
286
+ <span className="text-xs text-green-600 dark:text-green-400">
287
+ Fichier sain, aucune menace détectée
288
+ </span>
289
+ </div>
290
+ )}
291
+ </div>
292
+ </div>
293
+ <Button
294
+ size="sm"
295
+ variant="ghost"
296
+ onClick={() => onFileRemove?.(scannedFile.id)}
297
+ className="h-8 w-8 p-0"
298
+ >
299
+ <X className="h-4 w-4" />
300
+ </Button>
301
+ </div>
302
+ </div>
303
+ ))}
304
+ </div>
305
+ )}
306
+ </CardContent>
307
+ </Card>
308
+ )
309
+ }
310
+
311
+ export default FileScanUploader