@wakastellar/ui 2.1.2 → 2.3.2

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 (123) hide show
  1. package/dist/blocks/apm-overview/index.d.ts +58 -0
  2. package/dist/blocks/cicd-builder/index.d.ts +47 -0
  3. package/dist/blocks/cloud-cost-dashboard/index.d.ts +49 -0
  4. package/dist/blocks/container-orchestrator/index.d.ts +63 -0
  5. package/dist/blocks/database-admin/index.d.ts +84 -0
  6. package/dist/blocks/gitops-sync-status/index.d.ts +45 -0
  7. package/dist/blocks/incident-manager/index.d.ts +44 -0
  8. package/dist/blocks/index.d.ts +10 -0
  9. package/dist/blocks/infrastructure-map/index.d.ts +32 -0
  10. package/dist/blocks/on-call-schedule/index.d.ts +43 -0
  11. package/dist/blocks/release-notes/index.d.ts +49 -0
  12. package/dist/components/index.d.ts +34 -0
  13. package/dist/components/waka-ad-banner/index.d.ts +36 -0
  14. package/dist/components/waka-ad-fallback/index.d.ts +33 -0
  15. package/dist/components/waka-ad-inline/index.d.ts +15 -0
  16. package/dist/components/waka-ad-interstitial/index.d.ts +26 -0
  17. package/dist/components/waka-ad-placeholder/index.d.ts +17 -0
  18. package/dist/components/waka-ad-provider/index.d.ts +103 -0
  19. package/dist/components/waka-ad-sidebar/index.d.ts +18 -0
  20. package/dist/components/waka-ad-sticky-footer/index.d.ts +17 -0
  21. package/dist/components/waka-alert-panel/index.d.ts +45 -0
  22. package/dist/components/waka-artifact-list/index.d.ts +32 -0
  23. package/dist/components/waka-build-matrix/index.d.ts +36 -0
  24. package/dist/components/waka-config-comparator/index.d.ts +37 -0
  25. package/dist/components/waka-container-list/index.d.ts +51 -0
  26. package/dist/components/waka-content-recommendation/index.d.ts +23 -0
  27. package/dist/components/waka-database-card/index.d.ts +46 -0
  28. package/dist/components/waka-dependency-tree/index.d.ts +38 -0
  29. package/dist/components/waka-env-var-editor/index.d.ts +30 -0
  30. package/dist/components/waka-feature-flag-row/index.d.ts +45 -0
  31. package/dist/components/waka-kubernetes-overview/index.d.ts +98 -0
  32. package/dist/components/waka-log-viewer/index.d.ts +38 -0
  33. package/dist/components/waka-migration-list/index.d.ts +36 -0
  34. package/dist/components/waka-outstream-video/index.d.ts +24 -0
  35. package/dist/components/waka-pod-card/index.d.ts +73 -0
  36. package/dist/components/waka-query-explain/index.d.ts +48 -0
  37. package/dist/components/waka-secret-card/index.d.ts +43 -0
  38. package/dist/components/waka-security-scan-result/index.d.ts +45 -0
  39. package/dist/components/waka-service-graph/index.d.ts +44 -0
  40. package/dist/components/waka-sponsored-badge/index.d.ts +20 -0
  41. package/dist/components/waka-sponsored-card/index.d.ts +25 -0
  42. package/dist/components/waka-sponsored-feed/index.d.ts +31 -0
  43. package/dist/components/waka-test-report/index.d.ts +60 -0
  44. package/dist/components/waka-trace-viewer/index.d.ts +36 -0
  45. package/dist/components/waka-video-ad/index.d.ts +32 -0
  46. package/dist/components/waka-video-overlay/index.d.ts +26 -0
  47. package/dist/index.cjs.js +251 -200
  48. package/dist/index.d.ts +1 -0
  49. package/dist/index.es.js +47315 -35823
  50. package/dist/utils/security.d.ts +96 -0
  51. package/package.json +4 -4
  52. package/src/blocks/apm-overview/index.tsx +672 -0
  53. package/src/blocks/cicd-builder/index.tsx +738 -0
  54. package/src/blocks/cloud-cost-dashboard/index.tsx +597 -0
  55. package/src/blocks/container-orchestrator/index.tsx +729 -0
  56. package/src/blocks/database-admin/index.tsx +679 -0
  57. package/src/blocks/gitops-sync-status/index.tsx +557 -0
  58. package/src/blocks/incident-manager/index.tsx +586 -0
  59. package/src/blocks/index.ts +119 -0
  60. package/src/blocks/infrastructure-map/index.tsx +638 -0
  61. package/src/blocks/on-call-schedule/index.tsx +615 -0
  62. package/src/blocks/release-notes/index.tsx +643 -0
  63. package/src/blocks/sidebar/index.tsx +6 -6
  64. package/src/components/DataTable/templates/index.tsx +3 -2
  65. package/src/components/index.ts +283 -0
  66. package/src/components/waka-3d-pie-chart/index.tsx +11 -11
  67. package/src/components/waka-achievement-unlock/index.tsx +16 -16
  68. package/src/components/waka-ad-banner/index.tsx +275 -0
  69. package/src/components/waka-ad-fallback/index.tsx +181 -0
  70. package/src/components/waka-ad-inline/index.tsx +103 -0
  71. package/src/components/waka-ad-interstitial/index.tsx +278 -0
  72. package/src/components/waka-ad-placeholder/index.tsx +84 -0
  73. package/src/components/waka-ad-provider/index.tsx +329 -0
  74. package/src/components/waka-ad-sidebar/index.tsx +113 -0
  75. package/src/components/waka-ad-sticky-footer/index.tsx +125 -0
  76. package/src/components/waka-alert-panel/index.tsx +493 -0
  77. package/src/components/waka-artifact-list/index.tsx +416 -0
  78. package/src/components/waka-badge-showcase/index.tsx +12 -11
  79. package/src/components/waka-build-matrix/index.tsx +396 -0
  80. package/src/components/waka-command-bar/index.tsx +2 -1
  81. package/src/components/waka-config-comparator/index.tsx +416 -0
  82. package/src/components/waka-container-list/index.tsx +475 -0
  83. package/src/components/waka-content-recommendation/index.tsx +294 -0
  84. package/src/components/waka-cost-breakdown/index.tsx +10 -10
  85. package/src/components/waka-database-card/index.tsx +473 -0
  86. package/src/components/waka-dependency-tree/index.tsx +542 -0
  87. package/src/components/waka-env-var-editor/index.tsx +417 -0
  88. package/src/components/waka-feature-flag-row/index.tsx +386 -0
  89. package/src/components/waka-funnel-chart/index.tsx +8 -8
  90. package/src/components/waka-health-pulse/index.tsx +6 -6
  91. package/src/components/waka-kubernetes-overview/index.tsx +536 -0
  92. package/src/components/waka-leaderboard/index.tsx +9 -9
  93. package/src/components/waka-log-viewer/index.tsx +386 -0
  94. package/src/components/waka-loot-box/index.tsx +20 -20
  95. package/src/components/waka-migration-list/index.tsx +487 -0
  96. package/src/components/waka-outstream-video/index.tsx +240 -0
  97. package/src/components/waka-player-card/index.tsx +5 -5
  98. package/src/components/waka-pod-card/index.tsx +528 -0
  99. package/src/components/waka-query-explain/index.tsx +657 -0
  100. package/src/components/waka-quota-bar/index.tsx +4 -4
  101. package/src/components/waka-radar-score/index.tsx +10 -10
  102. package/src/components/waka-scratch-card/index.tsx +5 -4
  103. package/src/components/waka-secret-card/index.tsx +371 -0
  104. package/src/components/waka-security-scan-result/index.tsx +473 -0
  105. package/src/components/waka-server-rack/index.tsx +28 -27
  106. package/src/components/waka-service-graph/index.tsx +445 -0
  107. package/src/components/waka-sponsored-badge/index.tsx +97 -0
  108. package/src/components/waka-sponsored-card/index.tsx +275 -0
  109. package/src/components/waka-sponsored-feed/index.tsx +127 -0
  110. package/src/components/waka-spotlight/index.tsx +2 -1
  111. package/src/components/waka-success-explosion/index.tsx +4 -4
  112. package/src/components/waka-test-report/index.tsx +469 -0
  113. package/src/components/waka-trace-viewer/index.tsx +490 -0
  114. package/src/components/waka-video-ad/index.tsx +406 -0
  115. package/src/components/waka-video-overlay/index.tsx +257 -0
  116. package/src/components/waka-xp-bar/index.tsx +13 -13
  117. package/src/styles/base.css +16 -0
  118. package/src/styles/tailwind.preset.js +12 -0
  119. package/src/styles/themes/forest.css +16 -0
  120. package/src/styles/themes/monochrome.css +16 -0
  121. package/src/styles/themes/perpetuity.css +16 -0
  122. package/src/styles/themes/sunset.css +16 -0
  123. package/src/styles/themes/twilight.css +16 -0
@@ -0,0 +1,406 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { useRef, useState, useEffect, useCallback } from "react"
5
+ import { cn } from "../../utils/cn"
6
+ import { useAdContext, useAdVisibility, type CustomAd } from "../waka-ad-provider"
7
+ import { WakaSponsoredBadge } from "../waka-sponsored-badge"
8
+ import { WakaAdPlaceholder } from "../waka-ad-placeholder"
9
+ import { Play, Pause, Volume2, VolumeX, Maximize, ExternalLink } from "lucide-react"
10
+
11
+ export interface WakaVideoAdProps {
12
+ /** Unique slot ID */
13
+ slotId: string
14
+ /** Video width */
15
+ width?: number
16
+ /** Video height */
17
+ height?: number
18
+ /** Aspect ratio */
19
+ aspectRatio?: "16/9" | "4/3" | "1/1" | "9/16"
20
+ /** Autoplay when visible */
21
+ autoplay?: boolean
22
+ /** Mute by default */
23
+ muted?: boolean
24
+ /** Show controls */
25
+ showControls?: boolean
26
+ /** Skip time in seconds (0 = no skip) */
27
+ skipAfter?: number
28
+ /** Show progress bar */
29
+ showProgress?: boolean
30
+ /** CTA overlay position */
31
+ ctaPosition?: "bottom" | "overlay" | "end"
32
+ /** Custom class name */
33
+ className?: string
34
+ /** Callback when video completes */
35
+ onComplete?: () => void
36
+ /** Callback when skipped */
37
+ onSkip?: () => void
38
+ /** Callback when clicked */
39
+ onClick?: () => void
40
+ }
41
+
42
+ export function WakaVideoAd({
43
+ slotId,
44
+ width = 640,
45
+ height = 360,
46
+ aspectRatio = "16/9",
47
+ autoplay = true,
48
+ muted = true,
49
+ showControls = true,
50
+ skipAfter = 5,
51
+ showProgress = true,
52
+ ctaPosition = "bottom",
53
+ className,
54
+ onComplete,
55
+ onSkip,
56
+ onClick,
57
+ }: WakaVideoAdProps) {
58
+ const containerRef = useRef<HTMLDivElement>(null)
59
+ const videoRef = useRef<HTMLVideoElement>(null)
60
+ const { config, isReady, hasConsent, getCustomAd, trackEvent } = useAdContext()
61
+ const { isVisible } = useAdVisibility(containerRef)
62
+
63
+ const [status, setStatus] = useState<"loading" | "loaded" | "error">("loading")
64
+ const [ad, setAd] = useState<CustomAd | null>(null)
65
+ const [isPlaying, setIsPlaying] = useState(false)
66
+ const [isMuted, setIsMuted] = useState(muted)
67
+ const [progress, setProgress] = useState(0)
68
+ const [duration, setDuration] = useState(0)
69
+ const [canSkip, setCanSkip] = useState(skipAfter === 0)
70
+ const [skipCountdown, setSkipCountdown] = useState(skipAfter)
71
+ const [showCta, setShowCta] = useState(false)
72
+
73
+ // Load ad
74
+ useEffect(() => {
75
+ if (!isReady || hasConsent === false) return
76
+
77
+ const loadAd = async () => {
78
+ try {
79
+ const customAd = await getCustomAd(slotId)
80
+ if (customAd?.videoUrl) {
81
+ setAd(customAd)
82
+ setStatus("loaded")
83
+ trackEvent({ type: "loaded", slotId, timestamp: new Date() })
84
+ } else {
85
+ setStatus("error")
86
+ }
87
+ } catch {
88
+ setStatus("error")
89
+ }
90
+ }
91
+
92
+ loadAd()
93
+ }, [isReady, hasConsent, slotId, getCustomAd, trackEvent])
94
+
95
+ // Auto-play when visible
96
+ useEffect(() => {
97
+ if (!videoRef.current || !autoplay || status !== "loaded") return
98
+
99
+ if (isVisible) {
100
+ videoRef.current.play().catch(() => {})
101
+ } else {
102
+ videoRef.current.pause()
103
+ }
104
+ }, [isVisible, autoplay, status])
105
+
106
+ // Track impression when starts playing
107
+ useEffect(() => {
108
+ if (isPlaying && ad) {
109
+ trackEvent({ type: "impression", slotId, timestamp: new Date() })
110
+ if (ad.impressionUrl) {
111
+ fetch(ad.impressionUrl, { mode: "no-cors" }).catch(() => {})
112
+ }
113
+ }
114
+ }, [isPlaying, ad, slotId, trackEvent])
115
+
116
+ // Skip countdown
117
+ useEffect(() => {
118
+ if (!isPlaying || canSkip || skipAfter === 0) return
119
+
120
+ const interval = setInterval(() => {
121
+ setSkipCountdown((prev) => {
122
+ if (prev <= 1) {
123
+ setCanSkip(true)
124
+ return 0
125
+ }
126
+ return prev - 1
127
+ })
128
+ }, 1000)
129
+
130
+ return () => clearInterval(interval)
131
+ }, [isPlaying, canSkip, skipAfter])
132
+
133
+ const handlePlay = () => {
134
+ videoRef.current?.play()
135
+ }
136
+
137
+ const handlePause = () => {
138
+ videoRef.current?.pause()
139
+ }
140
+
141
+ const handleTogglePlay = () => {
142
+ if (isPlaying) {
143
+ handlePause()
144
+ } else {
145
+ handlePlay()
146
+ }
147
+ }
148
+
149
+ const handleToggleMute = () => {
150
+ if (videoRef.current) {
151
+ videoRef.current.muted = !isMuted
152
+ setIsMuted(!isMuted)
153
+ }
154
+ }
155
+
156
+ const handleFullscreen = () => {
157
+ containerRef.current?.requestFullscreen?.()
158
+ }
159
+
160
+ const handleTimeUpdate = () => {
161
+ if (videoRef.current) {
162
+ const current = videoRef.current.currentTime
163
+ const total = videoRef.current.duration
164
+ setProgress((current / total) * 100)
165
+
166
+ // Show CTA near end
167
+ if (ctaPosition === "end" && total - current < 5) {
168
+ setShowCta(true)
169
+ }
170
+ }
171
+ }
172
+
173
+ const handleLoadedMetadata = () => {
174
+ if (videoRef.current) {
175
+ setDuration(videoRef.current.duration)
176
+ }
177
+ }
178
+
179
+ const handleEnded = () => {
180
+ setIsPlaying(false)
181
+ setShowCta(true)
182
+ trackEvent({ type: "viewable", slotId, timestamp: new Date(), data: { completed: true } })
183
+ onComplete?.()
184
+ }
185
+
186
+ const handleSkip = () => {
187
+ if (!canSkip) return
188
+ videoRef.current?.pause()
189
+ setShowCta(true)
190
+ onSkip?.()
191
+ }
192
+
193
+ const handleClick = useCallback(() => {
194
+ if (!ad) return
195
+
196
+ trackEvent({ type: "click", slotId, timestamp: new Date() })
197
+ onClick?.()
198
+
199
+ if (ad.clickUrl) {
200
+ fetch(ad.clickUrl, { mode: "no-cors" }).catch(() => {})
201
+ }
202
+ if (ad.targetUrl) {
203
+ window.open(ad.targetUrl, "_blank", "noopener,noreferrer")
204
+ }
205
+ }, [ad, slotId, trackEvent, onClick])
206
+
207
+ const formatTime = (seconds: number) => {
208
+ const mins = Math.floor(seconds / 60)
209
+ const secs = Math.floor(seconds % 60)
210
+ return `${mins}:${secs.toString().padStart(2, "0")}`
211
+ }
212
+
213
+ if (status === "error") {
214
+ return null
215
+ }
216
+
217
+ if (status === "loading") {
218
+ return (
219
+ <div ref={containerRef}>
220
+ <WakaAdPlaceholder width={width} height={height} />
221
+ </div>
222
+ )
223
+ }
224
+
225
+ return (
226
+ <div
227
+ ref={containerRef}
228
+ className={cn(
229
+ "relative overflow-hidden rounded-lg bg-black group",
230
+ className
231
+ )}
232
+ style={{ width, maxWidth: "100%" }}
233
+ >
234
+ <div className={cn("relative", `aspect-[${aspectRatio}]`)}>
235
+ {/* Video */}
236
+ <video
237
+ ref={videoRef}
238
+ src={ad?.videoUrl}
239
+ muted={isMuted}
240
+ playsInline
241
+ onClick={handleTogglePlay}
242
+ onPlay={() => setIsPlaying(true)}
243
+ onPause={() => setIsPlaying(false)}
244
+ onTimeUpdate={handleTimeUpdate}
245
+ onLoadedMetadata={handleLoadedMetadata}
246
+ onEnded={handleEnded}
247
+ className="w-full h-full object-contain cursor-pointer"
248
+ />
249
+
250
+ {/* Sponsored badge */}
251
+ <div className="absolute top-2 left-2 z-10">
252
+ <WakaSponsoredBadge variant="dark" size="sm" sponsor={ad?.sponsor} />
253
+ </div>
254
+
255
+ {/* Skip button */}
256
+ {skipAfter > 0 && (
257
+ <div className="absolute top-2 right-2 z-10">
258
+ {canSkip ? (
259
+ <button
260
+ onClick={handleSkip}
261
+ className="px-3 py-1.5 bg-white/90 text-black text-sm font-medium rounded hover:bg-white transition-colors"
262
+ >
263
+ Skip Ad
264
+ </button>
265
+ ) : (
266
+ <div className="px-3 py-1.5 bg-black/50 text-white text-sm rounded">
267
+ Skip in {skipCountdown}s
268
+ </div>
269
+ )}
270
+ </div>
271
+ )}
272
+
273
+ {/* Play/Pause overlay */}
274
+ {!isPlaying && (
275
+ <button
276
+ onClick={handlePlay}
277
+ className="absolute inset-0 flex items-center justify-center bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity"
278
+ >
279
+ <Play className="h-16 w-16 text-white fill-white" />
280
+ </button>
281
+ )}
282
+
283
+ {/* CTA overlay */}
284
+ {ctaPosition === "overlay" && ad?.cta && (
285
+ <button
286
+ onClick={handleClick}
287
+ className="absolute bottom-16 left-1/2 -translate-x-1/2 px-6 py-2 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-primary/90 transition-colors flex items-center gap-2"
288
+ >
289
+ {ad.cta}
290
+ <ExternalLink className="h-4 w-4" />
291
+ </button>
292
+ )}
293
+
294
+ {/* End screen with CTA */}
295
+ {showCta && ctaPosition === "end" && (
296
+ <div className="absolute inset-0 flex flex-col items-center justify-center bg-black/80 p-6">
297
+ {ad?.imageUrl && (
298
+ <img
299
+ src={ad.imageUrl}
300
+ alt=""
301
+ className="w-24 h-24 rounded-lg object-cover mb-4"
302
+ />
303
+ )}
304
+ <h3 className="text-white text-xl font-bold text-center mb-2">
305
+ {ad?.title}
306
+ </h3>
307
+ {ad?.description && (
308
+ <p className="text-white/80 text-center mb-4 max-w-sm">
309
+ {ad.description}
310
+ </p>
311
+ )}
312
+ <button
313
+ onClick={handleClick}
314
+ className="px-6 py-3 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-primary/90 transition-colors flex items-center gap-2"
315
+ >
316
+ {ad?.cta || "Learn More"}
317
+ <ExternalLink className="h-4 w-4" />
318
+ </button>
319
+ </div>
320
+ )}
321
+
322
+ {/* Controls */}
323
+ {showControls && (
324
+ <div className="absolute bottom-0 left-0 right-0 p-3 bg-gradient-to-t from-black/80 to-transparent opacity-0 group-hover:opacity-100 transition-opacity">
325
+ {/* Progress bar */}
326
+ {showProgress && (
327
+ <div className="mb-2">
328
+ <div className="h-1 bg-white/30 rounded-full overflow-hidden">
329
+ <div
330
+ className="h-full bg-primary transition-all"
331
+ style={{ width: `${progress}%` }}
332
+ />
333
+ </div>
334
+ </div>
335
+ )}
336
+
337
+ <div className="flex items-center justify-between">
338
+ <div className="flex items-center gap-2">
339
+ <button
340
+ onClick={handleTogglePlay}
341
+ className="p-1.5 hover:bg-white/20 rounded transition-colors"
342
+ >
343
+ {isPlaying ? (
344
+ <Pause className="h-5 w-5 text-white" />
345
+ ) : (
346
+ <Play className="h-5 w-5 text-white fill-white" />
347
+ )}
348
+ </button>
349
+
350
+ <button
351
+ onClick={handleToggleMute}
352
+ className="p-1.5 hover:bg-white/20 rounded transition-colors"
353
+ >
354
+ {isMuted ? (
355
+ <VolumeX className="h-5 w-5 text-white" />
356
+ ) : (
357
+ <Volume2 className="h-5 w-5 text-white" />
358
+ )}
359
+ </button>
360
+
361
+ <span className="text-white text-xs">
362
+ {formatTime((progress / 100) * duration)} / {formatTime(duration)}
363
+ </span>
364
+ </div>
365
+
366
+ <button
367
+ onClick={handleFullscreen}
368
+ className="p-1.5 hover:bg-white/20 rounded transition-colors"
369
+ >
370
+ <Maximize className="h-5 w-5 text-white" />
371
+ </button>
372
+ </div>
373
+ </div>
374
+ )}
375
+ </div>
376
+
377
+ {/* Bottom CTA */}
378
+ {ctaPosition === "bottom" && ad?.cta && (
379
+ <button
380
+ onClick={handleClick}
381
+ className="w-full px-4 py-3 bg-muted hover:bg-muted/80 transition-colors flex items-center justify-between"
382
+ >
383
+ <div className="flex items-center gap-3">
384
+ {ad.imageUrl && (
385
+ <img
386
+ src={ad.imageUrl}
387
+ alt=""
388
+ className="w-10 h-10 rounded object-cover"
389
+ />
390
+ )}
391
+ <div className="text-left">
392
+ <p className="font-medium text-sm">{ad.title}</p>
393
+ <p className="text-xs text-muted-foreground">{ad.sponsor}</p>
394
+ </div>
395
+ </div>
396
+ <span className="px-4 py-1.5 bg-primary text-primary-foreground rounded text-sm font-medium flex items-center gap-1">
397
+ {ad.cta}
398
+ <ExternalLink className="h-3 w-3" />
399
+ </span>
400
+ </button>
401
+ )}
402
+ </div>
403
+ )
404
+ }
405
+
406
+ export default WakaVideoAd
@@ -0,0 +1,257 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { useState, useEffect, useCallback } from "react"
5
+ import { cn } from "../../utils/cn"
6
+ import { useAdContext, type CustomAd } from "../waka-ad-provider"
7
+ import { WakaSponsoredBadge } from "../waka-sponsored-badge"
8
+ import { X, ExternalLink } from "lucide-react"
9
+
10
+ export type OverlayPosition = "bottom" | "top" | "corner"
11
+ export type OverlayTrigger = "start" | "pause" | "time" | "manual"
12
+
13
+ export interface WakaVideoOverlayProps {
14
+ /** Unique slot ID */
15
+ slotId: string
16
+ /** Whether overlay is visible */
17
+ visible?: boolean
18
+ /** Trigger for showing overlay */
19
+ trigger?: OverlayTrigger
20
+ /** Time trigger (seconds from start) */
21
+ triggerTime?: number
22
+ /** Position of overlay */
23
+ position?: OverlayPosition
24
+ /** Allow dismissing */
25
+ dismissable?: boolean
26
+ /** Auto-hide after seconds (0 = manual) */
27
+ hideAfter?: number
28
+ /** Custom class name */
29
+ className?: string
30
+ /** Callback when dismissed */
31
+ onDismiss?: () => void
32
+ /** Callback when clicked */
33
+ onClick?: () => void
34
+ }
35
+
36
+ export function WakaVideoOverlay({
37
+ slotId,
38
+ visible = true,
39
+ trigger = "manual",
40
+ triggerTime = 0,
41
+ position = "bottom",
42
+ dismissable = true,
43
+ hideAfter = 0,
44
+ className,
45
+ onDismiss,
46
+ onClick,
47
+ }: WakaVideoOverlayProps) {
48
+ const { config, isReady, hasConsent, getCustomAd, trackEvent } = useAdContext()
49
+
50
+ const [status, setStatus] = useState<"loading" | "loaded" | "error" | "hidden">("loading")
51
+ const [ad, setAd] = useState<CustomAd | null>(null)
52
+ const [isVisible, setIsVisible] = useState(visible)
53
+
54
+ // Load ad
55
+ useEffect(() => {
56
+ if (!isReady || hasConsent === false) return
57
+
58
+ const loadAd = async () => {
59
+ try {
60
+ const customAd = await getCustomAd(slotId)
61
+ if (customAd) {
62
+ setAd(customAd)
63
+ setStatus("loaded")
64
+ trackEvent({ type: "loaded", slotId, timestamp: new Date() })
65
+ } else {
66
+ setStatus("error")
67
+ }
68
+ } catch {
69
+ setStatus("error")
70
+ }
71
+ }
72
+
73
+ loadAd()
74
+ }, [isReady, hasConsent, slotId, getCustomAd, trackEvent])
75
+
76
+ // Track impression when visible
77
+ useEffect(() => {
78
+ if (isVisible && status === "loaded" && ad) {
79
+ trackEvent({ type: "impression", slotId, timestamp: new Date() })
80
+ if (ad.impressionUrl) {
81
+ fetch(ad.impressionUrl, { mode: "no-cors" }).catch(() => {})
82
+ }
83
+ }
84
+ }, [isVisible, status, ad, slotId, trackEvent])
85
+
86
+ // Update visibility based on prop
87
+ useEffect(() => {
88
+ setIsVisible(visible)
89
+ }, [visible])
90
+
91
+ // Auto-hide timer
92
+ useEffect(() => {
93
+ if (hideAfter === 0 || !isVisible) return
94
+
95
+ const timer = setTimeout(() => {
96
+ setIsVisible(false)
97
+ setStatus("hidden")
98
+ onDismiss?.()
99
+ }, hideAfter * 1000)
100
+
101
+ return () => clearTimeout(timer)
102
+ }, [hideAfter, isVisible, onDismiss])
103
+
104
+ const handleDismiss = useCallback(() => {
105
+ if (!dismissable) return
106
+ setIsVisible(false)
107
+ setStatus("hidden")
108
+ onDismiss?.()
109
+ }, [dismissable, onDismiss])
110
+
111
+ const handleClick = useCallback(() => {
112
+ if (!ad) return
113
+
114
+ trackEvent({ type: "click", slotId, timestamp: new Date() })
115
+ onClick?.()
116
+
117
+ if (ad.clickUrl) {
118
+ fetch(ad.clickUrl, { mode: "no-cors" }).catch(() => {})
119
+ }
120
+ if (ad.targetUrl) {
121
+ window.open(ad.targetUrl, "_blank", "noopener,noreferrer")
122
+ }
123
+ }, [ad, slotId, trackEvent, onClick])
124
+
125
+ if (status === "error" || status === "hidden" || !isVisible) {
126
+ return null
127
+ }
128
+
129
+ if (status === "loading") {
130
+ return null
131
+ }
132
+
133
+ const positionClasses = {
134
+ bottom: "bottom-0 left-0 right-0",
135
+ top: "top-0 left-0 right-0",
136
+ corner: "bottom-4 right-4 max-w-sm",
137
+ }
138
+
139
+ // Corner position (card style)
140
+ if (position === "corner") {
141
+ return (
142
+ <div
143
+ className={cn(
144
+ "absolute z-20 rounded-lg overflow-hidden shadow-lg bg-card border animate-in slide-in-from-right",
145
+ positionClasses[position],
146
+ className
147
+ )}
148
+ >
149
+ {dismissable && (
150
+ <button
151
+ onClick={handleDismiss}
152
+ className="absolute top-2 right-2 p-1 rounded-full bg-black/50 hover:bg-black/70 transition-colors z-10"
153
+ >
154
+ <X className="h-3 w-3 text-white" />
155
+ </button>
156
+ )}
157
+
158
+ <button onClick={handleClick} className="w-full text-left">
159
+ {ad?.imageUrl && (
160
+ <div className="aspect-video bg-muted">
161
+ <img
162
+ src={ad.imageUrl}
163
+ alt=""
164
+ className="w-full h-full object-cover"
165
+ />
166
+ </div>
167
+ )}
168
+
169
+ <div className="p-3">
170
+ <div className="flex items-start justify-between gap-2 mb-1">
171
+ <p className="font-medium text-sm line-clamp-2">{ad?.title}</p>
172
+ </div>
173
+
174
+ {ad?.description && (
175
+ <p className="text-xs text-muted-foreground line-clamp-2 mb-2">
176
+ {ad.description}
177
+ </p>
178
+ )}
179
+
180
+ <div className="flex items-center justify-between">
181
+ <WakaSponsoredBadge size="sm" sponsor={ad?.sponsor} showIcon={false} />
182
+ {ad?.cta && (
183
+ <span className="text-xs text-primary font-medium flex items-center gap-1">
184
+ {ad.cta}
185
+ <ExternalLink className="h-3 w-3" />
186
+ </span>
187
+ )}
188
+ </div>
189
+ </div>
190
+ </button>
191
+ </div>
192
+ )
193
+ }
194
+
195
+ // Banner style (top/bottom)
196
+ return (
197
+ <div
198
+ className={cn(
199
+ "absolute z-20 bg-gradient-to-t from-black/80 to-black/50 backdrop-blur-sm",
200
+ position === "top" && "bg-gradient-to-b",
201
+ positionClasses[position],
202
+ className
203
+ )}
204
+ >
205
+ <div className="flex items-center gap-4 p-3">
206
+ {ad?.imageUrl && (
207
+ <button onClick={handleClick} className="flex-shrink-0">
208
+ <img
209
+ src={ad.imageUrl}
210
+ alt=""
211
+ className="w-16 h-16 rounded object-cover"
212
+ />
213
+ </button>
214
+ )}
215
+
216
+ <div className="flex-1 min-w-0">
217
+ <div className="flex items-center gap-2 mb-0.5">
218
+ <WakaSponsoredBadge variant="dark" size="sm" showIcon={false} />
219
+ </div>
220
+ <button onClick={handleClick} className="text-left">
221
+ <p className="font-medium text-white text-sm line-clamp-1">
222
+ {ad?.title}
223
+ </p>
224
+ {ad?.description && (
225
+ <p className="text-xs text-white/70 line-clamp-1">
226
+ {ad.description}
227
+ </p>
228
+ )}
229
+ </button>
230
+ </div>
231
+
232
+ <div className="flex items-center gap-2 flex-shrink-0">
233
+ {ad?.cta && (
234
+ <button
235
+ onClick={handleClick}
236
+ className="px-4 py-2 bg-primary text-primary-foreground rounded text-sm font-medium hover:bg-primary/90 transition-colors flex items-center gap-1"
237
+ >
238
+ {ad.cta}
239
+ <ExternalLink className="h-3 w-3" />
240
+ </button>
241
+ )}
242
+
243
+ {dismissable && (
244
+ <button
245
+ onClick={handleDismiss}
246
+ className="p-1.5 rounded-full hover:bg-white/20 transition-colors"
247
+ >
248
+ <X className="h-4 w-4 text-white" />
249
+ </button>
250
+ )}
251
+ </div>
252
+ </div>
253
+ </div>
254
+ )
255
+ }
256
+
257
+ export default WakaVideoOverlay