@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.
- package/dist/blocks/apm-overview/index.d.ts +58 -0
- package/dist/blocks/cicd-builder/index.d.ts +47 -0
- package/dist/blocks/cloud-cost-dashboard/index.d.ts +49 -0
- package/dist/blocks/container-orchestrator/index.d.ts +63 -0
- package/dist/blocks/database-admin/index.d.ts +84 -0
- package/dist/blocks/gitops-sync-status/index.d.ts +45 -0
- package/dist/blocks/incident-manager/index.d.ts +44 -0
- package/dist/blocks/index.d.ts +10 -0
- package/dist/blocks/infrastructure-map/index.d.ts +32 -0
- package/dist/blocks/on-call-schedule/index.d.ts +43 -0
- package/dist/blocks/release-notes/index.d.ts +49 -0
- package/dist/components/index.d.ts +34 -0
- package/dist/components/waka-ad-banner/index.d.ts +36 -0
- package/dist/components/waka-ad-fallback/index.d.ts +33 -0
- package/dist/components/waka-ad-inline/index.d.ts +15 -0
- package/dist/components/waka-ad-interstitial/index.d.ts +26 -0
- package/dist/components/waka-ad-placeholder/index.d.ts +17 -0
- package/dist/components/waka-ad-provider/index.d.ts +103 -0
- package/dist/components/waka-ad-sidebar/index.d.ts +18 -0
- package/dist/components/waka-ad-sticky-footer/index.d.ts +17 -0
- package/dist/components/waka-alert-panel/index.d.ts +45 -0
- package/dist/components/waka-artifact-list/index.d.ts +32 -0
- package/dist/components/waka-build-matrix/index.d.ts +36 -0
- package/dist/components/waka-config-comparator/index.d.ts +37 -0
- package/dist/components/waka-container-list/index.d.ts +51 -0
- package/dist/components/waka-content-recommendation/index.d.ts +23 -0
- package/dist/components/waka-database-card/index.d.ts +46 -0
- package/dist/components/waka-dependency-tree/index.d.ts +38 -0
- package/dist/components/waka-env-var-editor/index.d.ts +30 -0
- package/dist/components/waka-feature-flag-row/index.d.ts +45 -0
- package/dist/components/waka-kubernetes-overview/index.d.ts +98 -0
- package/dist/components/waka-log-viewer/index.d.ts +38 -0
- package/dist/components/waka-migration-list/index.d.ts +36 -0
- package/dist/components/waka-outstream-video/index.d.ts +24 -0
- package/dist/components/waka-pod-card/index.d.ts +73 -0
- package/dist/components/waka-query-explain/index.d.ts +48 -0
- package/dist/components/waka-secret-card/index.d.ts +43 -0
- package/dist/components/waka-security-scan-result/index.d.ts +45 -0
- package/dist/components/waka-service-graph/index.d.ts +44 -0
- package/dist/components/waka-sponsored-badge/index.d.ts +20 -0
- package/dist/components/waka-sponsored-card/index.d.ts +25 -0
- package/dist/components/waka-sponsored-feed/index.d.ts +31 -0
- package/dist/components/waka-test-report/index.d.ts +60 -0
- package/dist/components/waka-trace-viewer/index.d.ts +36 -0
- package/dist/components/waka-video-ad/index.d.ts +32 -0
- package/dist/components/waka-video-overlay/index.d.ts +26 -0
- package/dist/index.cjs.js +251 -200
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +47315 -35823
- package/dist/utils/security.d.ts +96 -0
- package/package.json +4 -4
- package/src/blocks/apm-overview/index.tsx +672 -0
- package/src/blocks/cicd-builder/index.tsx +738 -0
- package/src/blocks/cloud-cost-dashboard/index.tsx +597 -0
- package/src/blocks/container-orchestrator/index.tsx +729 -0
- package/src/blocks/database-admin/index.tsx +679 -0
- package/src/blocks/gitops-sync-status/index.tsx +557 -0
- package/src/blocks/incident-manager/index.tsx +586 -0
- package/src/blocks/index.ts +119 -0
- package/src/blocks/infrastructure-map/index.tsx +638 -0
- package/src/blocks/on-call-schedule/index.tsx +615 -0
- package/src/blocks/release-notes/index.tsx +643 -0
- package/src/blocks/sidebar/index.tsx +6 -6
- package/src/components/DataTable/templates/index.tsx +3 -2
- package/src/components/index.ts +283 -0
- package/src/components/waka-3d-pie-chart/index.tsx +11 -11
- package/src/components/waka-achievement-unlock/index.tsx +16 -16
- package/src/components/waka-ad-banner/index.tsx +275 -0
- package/src/components/waka-ad-fallback/index.tsx +181 -0
- package/src/components/waka-ad-inline/index.tsx +103 -0
- package/src/components/waka-ad-interstitial/index.tsx +278 -0
- package/src/components/waka-ad-placeholder/index.tsx +84 -0
- package/src/components/waka-ad-provider/index.tsx +329 -0
- package/src/components/waka-ad-sidebar/index.tsx +113 -0
- package/src/components/waka-ad-sticky-footer/index.tsx +125 -0
- package/src/components/waka-alert-panel/index.tsx +493 -0
- package/src/components/waka-artifact-list/index.tsx +416 -0
- package/src/components/waka-badge-showcase/index.tsx +12 -11
- package/src/components/waka-build-matrix/index.tsx +396 -0
- package/src/components/waka-command-bar/index.tsx +2 -1
- package/src/components/waka-config-comparator/index.tsx +416 -0
- package/src/components/waka-container-list/index.tsx +475 -0
- package/src/components/waka-content-recommendation/index.tsx +294 -0
- package/src/components/waka-cost-breakdown/index.tsx +10 -10
- package/src/components/waka-database-card/index.tsx +473 -0
- package/src/components/waka-dependency-tree/index.tsx +542 -0
- package/src/components/waka-env-var-editor/index.tsx +417 -0
- package/src/components/waka-feature-flag-row/index.tsx +386 -0
- package/src/components/waka-funnel-chart/index.tsx +8 -8
- package/src/components/waka-health-pulse/index.tsx +6 -6
- package/src/components/waka-kubernetes-overview/index.tsx +536 -0
- package/src/components/waka-leaderboard/index.tsx +9 -9
- package/src/components/waka-log-viewer/index.tsx +386 -0
- package/src/components/waka-loot-box/index.tsx +20 -20
- package/src/components/waka-migration-list/index.tsx +487 -0
- package/src/components/waka-outstream-video/index.tsx +240 -0
- package/src/components/waka-player-card/index.tsx +5 -5
- package/src/components/waka-pod-card/index.tsx +528 -0
- package/src/components/waka-query-explain/index.tsx +657 -0
- package/src/components/waka-quota-bar/index.tsx +4 -4
- package/src/components/waka-radar-score/index.tsx +10 -10
- package/src/components/waka-scratch-card/index.tsx +5 -4
- package/src/components/waka-secret-card/index.tsx +371 -0
- package/src/components/waka-security-scan-result/index.tsx +473 -0
- package/src/components/waka-server-rack/index.tsx +28 -27
- package/src/components/waka-service-graph/index.tsx +445 -0
- package/src/components/waka-sponsored-badge/index.tsx +97 -0
- package/src/components/waka-sponsored-card/index.tsx +275 -0
- package/src/components/waka-sponsored-feed/index.tsx +127 -0
- package/src/components/waka-spotlight/index.tsx +2 -1
- package/src/components/waka-success-explosion/index.tsx +4 -4
- package/src/components/waka-test-report/index.tsx +469 -0
- package/src/components/waka-trace-viewer/index.tsx +490 -0
- package/src/components/waka-video-ad/index.tsx +406 -0
- package/src/components/waka-video-overlay/index.tsx +257 -0
- package/src/components/waka-xp-bar/index.tsx +13 -13
- package/src/styles/base.css +16 -0
- package/src/styles/tailwind.preset.js +12 -0
- package/src/styles/themes/forest.css +16 -0
- package/src/styles/themes/monochrome.css +16 -0
- package/src/styles/themes/perpetuity.css +16 -0
- package/src/styles/themes/sunset.css +16 -0
- 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
|