@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,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
|