@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,181 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils/cn"
5
+ import { AD_SIZES, type AdSize } from "../waka-ad-provider"
6
+
7
+ export type AdFallbackVariant = "house" | "empty" | "custom"
8
+
9
+ export interface WakaAdFallbackProps {
10
+ /** Fallback variant */
11
+ variant?: AdFallbackVariant
12
+ /** Ad size preset */
13
+ size?: AdSize
14
+ /** Width in pixels (overrides size) */
15
+ width?: number
16
+ /** Height in pixels (overrides size) */
17
+ height?: number
18
+ /** Title text */
19
+ title?: string
20
+ /** Description text */
21
+ description?: string
22
+ /** Call to action text */
23
+ ctaText?: string
24
+ /** Link URL */
25
+ ctaUrl?: string
26
+ /** Image URL */
27
+ imageUrl?: string
28
+ /** Show border */
29
+ showBorder?: boolean
30
+ /** Custom class name */
31
+ className?: string
32
+ /** Callback when clicked */
33
+ onClick?: () => void
34
+ /** Custom children for variant="custom" */
35
+ children?: React.ReactNode
36
+ }
37
+
38
+ export function WakaAdFallback({
39
+ variant = "house",
40
+ size = "rectangle",
41
+ width,
42
+ height,
43
+ title = "Your Ad Here",
44
+ description = "Advertise with us",
45
+ ctaText = "Learn More",
46
+ ctaUrl,
47
+ imageUrl,
48
+ showBorder = true,
49
+ className,
50
+ onClick,
51
+ children,
52
+ }: WakaAdFallbackProps) {
53
+ const sizeConfig = AD_SIZES[size]
54
+ const finalWidth = width ?? sizeConfig.width
55
+ const finalHeight = height ?? sizeConfig.height
56
+
57
+ // Custom variant renders children
58
+ if (variant === "custom" && children) {
59
+ return (
60
+ <div
61
+ className={cn("rounded-md overflow-hidden", className)}
62
+ style={{ width: finalWidth, height: finalHeight, maxWidth: "100%" }}
63
+ >
64
+ {children}
65
+ </div>
66
+ )
67
+ }
68
+
69
+ // Empty variant
70
+ if (variant === "empty") {
71
+ return (
72
+ <div
73
+ className={cn(
74
+ "relative overflow-hidden rounded-md",
75
+ showBorder && "border border-dashed border-muted-foreground/20",
76
+ "flex items-center justify-center",
77
+ "bg-muted/20",
78
+ className
79
+ )}
80
+ style={{ width: finalWidth, height: finalHeight, maxWidth: "100%" }}
81
+ >
82
+ <span className="text-xs text-muted-foreground/40">Ad</span>
83
+ </div>
84
+ )
85
+ }
86
+ // House variant (default)
87
+ const content = (
88
+ <div
89
+ className={cn(
90
+ "relative overflow-hidden rounded-md transition-all duration-200",
91
+ showBorder && "border border-dashed border-muted-foreground/30",
92
+ "hover:border-muted-foreground/50 hover:bg-muted/50",
93
+ "flex flex-col items-center justify-center p-4 text-center",
94
+ "bg-gradient-to-br from-muted/30 to-muted/10",
95
+ className
96
+ )}
97
+ style={{
98
+ width: finalWidth,
99
+ height: finalHeight,
100
+ maxWidth: "100%",
101
+ backgroundImage: imageUrl ? `url(${imageUrl})` : undefined,
102
+ backgroundSize: "cover",
103
+ backgroundPosition: "center",
104
+ }}
105
+ >
106
+ {imageUrl && (
107
+ <div className="absolute inset-0 bg-black/40" />
108
+ )}
109
+
110
+ <div className={cn("relative z-10", imageUrl && "text-white")}>
111
+ <div className="mb-2">
112
+ <svg
113
+ className="h-10 w-10 mx-auto text-muted-foreground/50"
114
+ fill="none"
115
+ viewBox="0 0 24 24"
116
+ stroke="currentColor"
117
+ >
118
+ <path
119
+ strokeLinecap="round"
120
+ strokeLinejoin="round"
121
+ strokeWidth={1.5}
122
+ d="M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z"
123
+ />
124
+ </svg>
125
+ </div>
126
+
127
+ <h3 className={cn(
128
+ "font-semibold text-sm mb-1",
129
+ !imageUrl && "text-muted-foreground"
130
+ )}>
131
+ {title}
132
+ </h3>
133
+
134
+ <p className={cn(
135
+ "text-xs mb-3",
136
+ !imageUrl ? "text-muted-foreground/70" : "text-white/80"
137
+ )}>
138
+ {description}
139
+ </p>
140
+
141
+ <span className={cn(
142
+ "inline-flex items-center px-3 py-1 rounded text-xs font-medium transition-colors",
143
+ imageUrl
144
+ ? "bg-white text-black hover:bg-white/90"
145
+ : "bg-primary/10 text-primary hover:bg-primary/20"
146
+ )}>
147
+ {ctaText}
148
+ <svg className="ml-1 h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
149
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
150
+ </svg>
151
+ </span>
152
+ </div>
153
+ </div>
154
+ )
155
+
156
+ if (ctaUrl) {
157
+ return (
158
+ <a
159
+ href={ctaUrl}
160
+ target="_blank"
161
+ rel="noopener noreferrer sponsored"
162
+ onClick={onClick}
163
+ className="block cursor-pointer"
164
+ >
165
+ {content}
166
+ </a>
167
+ )
168
+ }
169
+
170
+ if (onClick) {
171
+ return (
172
+ <button onClick={onClick} className="block cursor-pointer text-left">
173
+ {content}
174
+ </button>
175
+ )
176
+ }
177
+
178
+ return content
179
+ }
180
+
181
+ export default WakaAdFallback
@@ -0,0 +1,103 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils/cn"
5
+ import { WakaAdBanner, type WakaAdBannerProps } from "../waka-ad-banner"
6
+
7
+ export interface WakaAdInlineProps extends Omit<WakaAdBannerProps, "size"> {
8
+ /** Ad size - defaults to responsive */
9
+ size?: "responsive" | "rectangle" | "leaderboard"
10
+ /** Alignment within container */
11
+ align?: "left" | "center" | "right"
12
+ /** Vertical margin */
13
+ margin?: "none" | "sm" | "md" | "lg"
14
+ /** Show separator lines */
15
+ showSeparators?: boolean
16
+ /** Label above the ad */
17
+ label?: string
18
+ }
19
+
20
+ export function WakaAdInline({
21
+ slotId,
22
+ size = "responsive",
23
+ align = "center",
24
+ margin = "md",
25
+ showSeparators = false,
26
+ label,
27
+ className,
28
+ ...bannerProps
29
+ }: WakaAdInlineProps) {
30
+ const alignClasses = {
31
+ left: "justify-start",
32
+ center: "justify-center",
33
+ right: "justify-end",
34
+ }
35
+
36
+ const marginClasses = {
37
+ none: "my-0",
38
+ sm: "my-4",
39
+ md: "my-8",
40
+ lg: "my-12",
41
+ }
42
+
43
+ // Responsive sizing based on viewport
44
+ const getResponsiveSize = (): "leaderboard" | "rectangle" | "mobile-banner" => {
45
+ if (typeof window === "undefined") return "rectangle"
46
+ if (window.innerWidth >= 768) return "leaderboard"
47
+ if (window.innerWidth >= 480) return "rectangle"
48
+ return "mobile-banner"
49
+ }
50
+
51
+ const [responsiveSize, setResponsiveSize] = React.useState<"leaderboard" | "rectangle" | "mobile-banner">("rectangle")
52
+
53
+ React.useEffect(() => {
54
+ const handleResize = () => {
55
+ setResponsiveSize(getResponsiveSize())
56
+ }
57
+ handleResize()
58
+ window.addEventListener("resize", handleResize)
59
+ return () => window.removeEventListener("resize", handleResize)
60
+ }, [])
61
+
62
+ const actualSize = size === "responsive" ? responsiveSize : size
63
+
64
+ return (
65
+ <div
66
+ className={cn(
67
+ "w-full",
68
+ marginClasses[margin],
69
+ className
70
+ )}
71
+ >
72
+ {showSeparators && (
73
+ <div className="flex items-center gap-4 mb-3">
74
+ <div className="flex-1 h-px bg-border" />
75
+ <span className="text-xs text-muted-foreground uppercase tracking-wider">
76
+ {label || "Advertisement"}
77
+ </span>
78
+ <div className="flex-1 h-px bg-border" />
79
+ </div>
80
+ )}
81
+
82
+ {!showSeparators && label && (
83
+ <p className="text-xs text-muted-foreground uppercase tracking-wider mb-2 text-center">
84
+ {label}
85
+ </p>
86
+ )}
87
+
88
+ <div className={cn("flex w-full", alignClasses[align])}>
89
+ <WakaAdBanner
90
+ slotId={slotId}
91
+ size={actualSize}
92
+ {...bannerProps}
93
+ />
94
+ </div>
95
+
96
+ {showSeparators && (
97
+ <div className="h-px bg-border mt-3" />
98
+ )}
99
+ </div>
100
+ )
101
+ }
102
+
103
+ export default WakaAdInline
@@ -0,0 +1,278 @@
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 { WakaAdPlaceholder } from "../waka-ad-placeholder"
8
+ import { WakaSponsoredBadge } from "../waka-sponsored-badge"
9
+ import { X } from "lucide-react"
10
+
11
+ export interface WakaAdInterstitialProps {
12
+ /** Unique slot ID */
13
+ slotId: string
14
+ /** Whether the interstitial is open */
15
+ open: boolean
16
+ /** Callback when closed */
17
+ onClose: () => void
18
+ /** Minimum display time before close button appears (seconds) */
19
+ minDisplayTime?: number
20
+ /** Auto-close after duration (seconds, 0 = manual close only) */
21
+ autoCloseAfter?: number
22
+ /** GPT ad unit path */
23
+ adUnitPath?: string
24
+ /** Show countdown timer */
25
+ showCountdown?: boolean
26
+ /** Skip button text */
27
+ skipText?: string
28
+ /** Custom class name */
29
+ className?: string
30
+ /** Callback when ad loads */
31
+ onLoad?: () => void
32
+ /** Callback when ad is clicked */
33
+ onClick?: () => void
34
+ }
35
+
36
+ export function WakaAdInterstitial({
37
+ slotId,
38
+ open,
39
+ onClose,
40
+ minDisplayTime = 5,
41
+ autoCloseAfter = 0,
42
+ adUnitPath,
43
+ showCountdown = true,
44
+ skipText = "Skip Ad",
45
+ className,
46
+ onLoad,
47
+ onClick,
48
+ }: WakaAdInterstitialProps) {
49
+ const { config, isReady, hasConsent, getCustomAd, trackEvent } = useAdContext()
50
+
51
+ const [status, setStatus] = useState<"loading" | "loaded" | "error">("loading")
52
+ const [customAd, setCustomAd] = useState<CustomAd | null>(null)
53
+ const [countdown, setCountdown] = useState(minDisplayTime)
54
+ const [canSkip, setCanSkip] = useState(minDisplayTime === 0)
55
+ const [autoCloseCountdown, setAutoCloseCountdown] = useState(autoCloseAfter)
56
+
57
+ // Load ad
58
+ useEffect(() => {
59
+ if (!open || !isReady || hasConsent === false) return
60
+
61
+ const loadAd = async () => {
62
+ setStatus("loading")
63
+ setCountdown(minDisplayTime)
64
+ setCanSkip(minDisplayTime === 0)
65
+ setAutoCloseCountdown(autoCloseAfter)
66
+
67
+ try {
68
+ if (config.network === "custom") {
69
+ const ad = await getCustomAd(slotId)
70
+ if (ad) {
71
+ setCustomAd(ad)
72
+ setStatus("loaded")
73
+ trackEvent({ type: "loaded", slotId, timestamp: new Date() })
74
+ trackEvent({ type: "impression", slotId, timestamp: new Date() })
75
+ onLoad?.()
76
+
77
+ // Fire tracking
78
+ if (ad.impressionUrl) {
79
+ fetch(ad.impressionUrl, { mode: "no-cors" }).catch(() => {})
80
+ }
81
+ } else {
82
+ setStatus("error")
83
+ onClose()
84
+ }
85
+ } else {
86
+ // For GPT, we'd need to handle differently
87
+ setStatus("loaded")
88
+ onLoad?.()
89
+ }
90
+ } catch {
91
+ setStatus("error")
92
+ onClose()
93
+ }
94
+ }
95
+
96
+ loadAd()
97
+ }, [open, isReady, hasConsent, config.network, slotId, getCustomAd, trackEvent, minDisplayTime, autoCloseAfter, onLoad, onClose])
98
+
99
+ // Skip countdown
100
+ useEffect(() => {
101
+ if (!open || canSkip || countdown <= 0) return
102
+
103
+ const timer = setInterval(() => {
104
+ setCountdown((prev) => {
105
+ if (prev <= 1) {
106
+ setCanSkip(true)
107
+ return 0
108
+ }
109
+ return prev - 1
110
+ })
111
+ }, 1000)
112
+
113
+ return () => clearInterval(timer)
114
+ }, [open, canSkip, countdown])
115
+
116
+ // Auto-close countdown
117
+ useEffect(() => {
118
+ if (!open || autoCloseAfter === 0 || autoCloseCountdown <= 0) return
119
+
120
+ const timer = setInterval(() => {
121
+ setAutoCloseCountdown((prev) => {
122
+ if (prev <= 1) {
123
+ onClose()
124
+ return 0
125
+ }
126
+ return prev - 1
127
+ })
128
+ }, 1000)
129
+
130
+ return () => clearInterval(timer)
131
+ }, [open, autoCloseAfter, autoCloseCountdown, onClose])
132
+
133
+ // Track viewability
134
+ useEffect(() => {
135
+ if (open && status === "loaded") {
136
+ trackEvent({ type: "viewable", slotId, timestamp: new Date() })
137
+ }
138
+ }, [open, status, slotId, trackEvent])
139
+
140
+ const handleClick = useCallback(() => {
141
+ trackEvent({ type: "click", slotId, timestamp: new Date() })
142
+ onClick?.()
143
+
144
+ if (customAd?.clickUrl) {
145
+ fetch(customAd.clickUrl, { mode: "no-cors" }).catch(() => {})
146
+ }
147
+ if (customAd?.targetUrl) {
148
+ window.open(customAd.targetUrl, "_blank", "noopener,noreferrer")
149
+ }
150
+ }, [slotId, customAd, trackEvent, onClick])
151
+
152
+ const handleClose = useCallback(() => {
153
+ if (canSkip) {
154
+ onClose()
155
+ }
156
+ }, [canSkip, onClose])
157
+
158
+ // Handle escape key
159
+ useEffect(() => {
160
+ if (!open) return
161
+
162
+ const handleKeyDown = (e: KeyboardEvent) => {
163
+ if (e.key === "Escape" && canSkip) {
164
+ onClose()
165
+ }
166
+ }
167
+
168
+ document.addEventListener("keydown", handleKeyDown)
169
+ return () => document.removeEventListener("keydown", handleKeyDown)
170
+ }, [open, canSkip, onClose])
171
+
172
+ // Prevent body scroll when open
173
+ useEffect(() => {
174
+ if (open) {
175
+ document.body.style.overflow = "hidden"
176
+ return () => {
177
+ document.body.style.overflow = ""
178
+ }
179
+ }
180
+ }, [open])
181
+
182
+ if (!open) return null
183
+
184
+ return (
185
+ <div
186
+ className={cn(
187
+ "fixed inset-0 z-50 flex items-center justify-center",
188
+ "bg-black/80 backdrop-blur-sm",
189
+ className
190
+ )}
191
+ role="dialog"
192
+ aria-modal="true"
193
+ aria-label="Advertisement"
194
+ >
195
+ {/* Close/Skip button */}
196
+ <div className="absolute top-4 right-4 z-10">
197
+ {canSkip ? (
198
+ <button
199
+ onClick={handleClose}
200
+ className="flex items-center gap-2 px-4 py-2 bg-white/10 hover:bg-white/20 text-white rounded-full transition-colors"
201
+ >
202
+ <span className="text-sm font-medium">{skipText}</span>
203
+ <X className="h-4 w-4" />
204
+ </button>
205
+ ) : showCountdown ? (
206
+ <div className="flex items-center gap-2 px-4 py-2 bg-white/10 text-white rounded-full">
207
+ <span className="text-sm">Skip in</span>
208
+ <span className="w-6 h-6 flex items-center justify-center bg-white/20 rounded-full text-sm font-bold">
209
+ {countdown}
210
+ </span>
211
+ </div>
212
+ ) : null}
213
+ </div>
214
+
215
+ {/* Sponsored badge */}
216
+ <div className="absolute top-4 left-4 z-10">
217
+ <WakaSponsoredBadge
218
+ sponsor={customAd?.sponsor}
219
+ variant="dark"
220
+ size="md"
221
+ />
222
+ </div>
223
+
224
+ {/* Auto-close indicator */}
225
+ {autoCloseAfter > 0 && status === "loaded" && (
226
+ <div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10">
227
+ <div className="px-4 py-2 bg-white/10 text-white rounded-full text-sm">
228
+ Closing in {autoCloseCountdown}s
229
+ </div>
230
+ </div>
231
+ )}
232
+
233
+ {/* Ad content */}
234
+ <div className="relative max-w-4xl max-h-[80vh] w-full mx-4">
235
+ {status === "loading" ? (
236
+ <WakaAdPlaceholder width={800} height={600} />
237
+ ) : customAd ? (
238
+ <button
239
+ onClick={handleClick}
240
+ className="relative w-full focus:outline-none focus:ring-2 focus:ring-white/50 rounded-lg overflow-hidden"
241
+ >
242
+ {customAd.videoUrl ? (
243
+ <video
244
+ src={customAd.videoUrl}
245
+ autoPlay
246
+ muted
247
+ playsInline
248
+ className="w-full max-h-[80vh] object-contain bg-black"
249
+ />
250
+ ) : customAd.imageUrl ? (
251
+ <img
252
+ src={customAd.imageUrl}
253
+ alt={customAd.title || "Advertisement"}
254
+ className="w-full max-h-[80vh] object-contain"
255
+ />
256
+ ) : (
257
+ <div className="flex flex-col items-center justify-center h-96 bg-muted rounded-lg p-8">
258
+ <h2 className="text-2xl font-bold mb-2">{customAd.title}</h2>
259
+ {customAd.description && (
260
+ <p className="text-muted-foreground mb-4 text-center max-w-md">
261
+ {customAd.description}
262
+ </p>
263
+ )}
264
+ {customAd.cta && (
265
+ <span className="px-6 py-3 bg-primary text-primary-foreground rounded-lg font-medium">
266
+ {customAd.cta}
267
+ </span>
268
+ )}
269
+ </div>
270
+ )}
271
+ </button>
272
+ ) : null}
273
+ </div>
274
+ </div>
275
+ )
276
+ }
277
+
278
+ export default WakaAdInterstitial
@@ -0,0 +1,84 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils/cn"
5
+ import { AD_SIZES, type AdSize } from "../waka-ad-provider"
6
+
7
+ export interface WakaAdPlaceholderProps {
8
+ /** Ad size preset */
9
+ size?: AdSize
10
+ /** Width in pixels (overrides size) */
11
+ width?: number
12
+ /** Height in pixels (overrides size) */
13
+ height?: number
14
+ /** Show animated skeleton */
15
+ animated?: boolean
16
+ /** Show label text */
17
+ showLabel?: boolean
18
+ /** Custom class name */
19
+ className?: string
20
+ }
21
+
22
+ export function WakaAdPlaceholder({
23
+ size = "rectangle",
24
+ width,
25
+ height,
26
+ animated = true,
27
+ showLabel = true,
28
+ className,
29
+ }: WakaAdPlaceholderProps) {
30
+ const sizeConfig = AD_SIZES[size]
31
+ const finalWidth = width ?? sizeConfig.width
32
+ const finalHeight = height ?? sizeConfig.height
33
+ return (
34
+ <div
35
+ className={cn(
36
+ "relative overflow-hidden bg-muted rounded-md",
37
+ animated && "animate-pulse",
38
+ className
39
+ )}
40
+ style={{ width: finalWidth, height: finalHeight, maxWidth: "100%" }}
41
+ role="status"
42
+ aria-label="Loading advertisement"
43
+ >
44
+ <div className="absolute inset-0 flex items-center justify-center">
45
+ <div className="flex flex-col items-center gap-2 text-muted-foreground/50">
46
+ <svg
47
+ className="h-8 w-8"
48
+ fill="none"
49
+ viewBox="0 0 24 24"
50
+ stroke="currentColor"
51
+ >
52
+ <path
53
+ strokeLinecap="round"
54
+ strokeLinejoin="round"
55
+ strokeWidth={1.5}
56
+ d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
57
+ />
58
+ </svg>
59
+ {showLabel && <span className="text-xs">Loading ad...</span>}
60
+ </div>
61
+ </div>
62
+
63
+ {/* Shimmer effect */}
64
+ {animated && (
65
+ <div
66
+ className="absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-white/10 to-transparent animate-shimmer"
67
+ style={{
68
+ animation: "shimmer 2s infinite",
69
+ }}
70
+ />
71
+ )}
72
+
73
+ <style>{`
74
+ @keyframes shimmer {
75
+ 100% {
76
+ transform: translateX(100%);
77
+ }
78
+ }
79
+ `}</style>
80
+ </div>
81
+ )
82
+ }
83
+
84
+ export default WakaAdPlaceholder