@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,329 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { createContext, useContext, useState, useEffect, useCallback, useRef } from "react"
|
|
5
|
+
|
|
6
|
+
// ==================== Types ====================
|
|
7
|
+
|
|
8
|
+
export type AdNetwork = "gpt" | "custom" | "adsense"
|
|
9
|
+
export type AdSize = "leaderboard" | "rectangle" | "skyscraper" | "billboard" | "mobile-banner" | "custom"
|
|
10
|
+
export type AdPosition = "top" | "bottom" | "sidebar" | "inline" | "interstitial" | "sticky"
|
|
11
|
+
|
|
12
|
+
export interface AdSlot {
|
|
13
|
+
id: string
|
|
14
|
+
network: AdNetwork
|
|
15
|
+
size: AdSize
|
|
16
|
+
position: AdPosition
|
|
17
|
+
targeting?: Record<string, string | string[]>
|
|
18
|
+
refreshInterval?: number // in seconds, 0 = no refresh
|
|
19
|
+
lazyLoad?: boolean
|
|
20
|
+
customWidth?: number
|
|
21
|
+
customHeight?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AdConfig {
|
|
25
|
+
network: AdNetwork
|
|
26
|
+
publisherId?: string
|
|
27
|
+
// GPT specific
|
|
28
|
+
gptNetworkCode?: string
|
|
29
|
+
gptAdUnits?: Record<string, string>
|
|
30
|
+
// Custom ad server
|
|
31
|
+
customAdServer?: string
|
|
32
|
+
customApiKey?: string
|
|
33
|
+
// General settings
|
|
34
|
+
enableLazyLoad?: boolean
|
|
35
|
+
lazyLoadOffset?: number // pixels before viewport
|
|
36
|
+
enableRefresh?: boolean
|
|
37
|
+
defaultRefreshInterval?: number // seconds
|
|
38
|
+
enableConsent?: boolean
|
|
39
|
+
testMode?: boolean
|
|
40
|
+
debugMode?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AdEvent {
|
|
44
|
+
type: "impression" | "click" | "viewable" | "error" | "loaded" | "empty"
|
|
45
|
+
slotId: string
|
|
46
|
+
timestamp: Date
|
|
47
|
+
data?: Record<string, unknown>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface CustomAd {
|
|
51
|
+
id: string
|
|
52
|
+
imageUrl?: string
|
|
53
|
+
videoUrl?: string
|
|
54
|
+
targetUrl: string
|
|
55
|
+
title?: string
|
|
56
|
+
description?: string
|
|
57
|
+
sponsor?: string
|
|
58
|
+
cta?: string
|
|
59
|
+
trackingPixels?: string[]
|
|
60
|
+
impressionUrl?: string
|
|
61
|
+
clickUrl?: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface AdContextValue {
|
|
65
|
+
config: AdConfig
|
|
66
|
+
isReady: boolean
|
|
67
|
+
hasConsent: boolean | null
|
|
68
|
+
setConsent: (consent: boolean) => void
|
|
69
|
+
registerSlot: (slot: AdSlot) => void
|
|
70
|
+
unregisterSlot: (slotId: string) => void
|
|
71
|
+
refreshSlot: (slotId: string) => void
|
|
72
|
+
refreshAll: () => void
|
|
73
|
+
getCustomAd: (slotId: string) => Promise<CustomAd | null>
|
|
74
|
+
trackEvent: (event: AdEvent) => void
|
|
75
|
+
slots: Map<string, AdSlot>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ==================== Size Definitions ====================
|
|
79
|
+
|
|
80
|
+
export const AD_SIZES: Record<AdSize, { width: number; height: number; label: string }> = {
|
|
81
|
+
leaderboard: { width: 728, height: 90, label: "Leaderboard (728x90)" },
|
|
82
|
+
rectangle: { width: 300, height: 250, label: "Medium Rectangle (300x250)" },
|
|
83
|
+
skyscraper: { width: 160, height: 600, label: "Wide Skyscraper (160x600)" },
|
|
84
|
+
billboard: { width: 970, height: 250, label: "Billboard (970x250)" },
|
|
85
|
+
"mobile-banner": { width: 320, height: 50, label: "Mobile Banner (320x50)" },
|
|
86
|
+
custom: { width: 0, height: 0, label: "Custom Size" },
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ==================== Context ====================
|
|
90
|
+
|
|
91
|
+
const AdContext = createContext<AdContextValue | null>(null)
|
|
92
|
+
|
|
93
|
+
export function useAdContext() {
|
|
94
|
+
const context = useContext(AdContext)
|
|
95
|
+
if (!context) {
|
|
96
|
+
throw new Error("useAdContext must be used within WakaAdProvider")
|
|
97
|
+
}
|
|
98
|
+
return context
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ==================== Hooks ====================
|
|
102
|
+
|
|
103
|
+
export function useAdVisibility(ref: React.RefObject<HTMLElement>, threshold = 0.5) {
|
|
104
|
+
const [isVisible, setIsVisible] = useState(false)
|
|
105
|
+
const [viewableTime, setViewableTime] = useState(0)
|
|
106
|
+
const viewableStartRef = useRef<number | null>(null)
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!ref.current) return
|
|
110
|
+
|
|
111
|
+
const observer = new IntersectionObserver(
|
|
112
|
+
([entry]) => {
|
|
113
|
+
const nowVisible = entry.intersectionRatio >= threshold
|
|
114
|
+
setIsVisible(nowVisible)
|
|
115
|
+
|
|
116
|
+
if (nowVisible && !viewableStartRef.current) {
|
|
117
|
+
viewableStartRef.current = Date.now()
|
|
118
|
+
} else if (!nowVisible && viewableStartRef.current) {
|
|
119
|
+
setViewableTime((prev) => prev + (Date.now() - viewableStartRef.current!))
|
|
120
|
+
viewableStartRef.current = null
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{ threshold: [0, threshold, 1] }
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
observer.observe(ref.current)
|
|
127
|
+
return () => observer.disconnect()
|
|
128
|
+
}, [ref, threshold])
|
|
129
|
+
|
|
130
|
+
return { isVisible, viewableTime }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function useAdConsent() {
|
|
134
|
+
const { hasConsent, setConsent } = useAdContext()
|
|
135
|
+
return { hasConsent, setConsent }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function useAdSlot(slot: AdSlot) {
|
|
139
|
+
const { registerSlot, unregisterSlot, refreshSlot, isReady } = useAdContext()
|
|
140
|
+
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (isReady) {
|
|
143
|
+
registerSlot(slot)
|
|
144
|
+
return () => unregisterSlot(slot.id)
|
|
145
|
+
}
|
|
146
|
+
}, [slot.id, isReady, registerSlot, unregisterSlot, slot])
|
|
147
|
+
|
|
148
|
+
const refresh = useCallback(() => refreshSlot(slot.id), [slot.id, refreshSlot])
|
|
149
|
+
|
|
150
|
+
return { refresh, isReady }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ==================== Provider ====================
|
|
154
|
+
|
|
155
|
+
interface WakaAdProviderProps {
|
|
156
|
+
children: React.ReactNode
|
|
157
|
+
config: AdConfig
|
|
158
|
+
onEvent?: (event: AdEvent) => void
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function WakaAdProvider({ children, config, onEvent }: WakaAdProviderProps) {
|
|
162
|
+
const [isReady, setIsReady] = useState(false)
|
|
163
|
+
const [hasConsent, setHasConsent] = useState<boolean | null>(null)
|
|
164
|
+
const [slots, setSlots] = useState<Map<string, AdSlot>>(new Map())
|
|
165
|
+
const eventQueueRef = useRef<AdEvent[]>([])
|
|
166
|
+
|
|
167
|
+
// Initialize ad network
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
const initNetwork = async () => {
|
|
170
|
+
if (config.network === "gpt" && typeof window !== "undefined") {
|
|
171
|
+
// Load GPT script
|
|
172
|
+
if (!window.googletag) {
|
|
173
|
+
const script = document.createElement("script")
|
|
174
|
+
script.src = "https://securepubads.g.doubleclick.net/tag/js/gpt.js"
|
|
175
|
+
script.async = true
|
|
176
|
+
document.head.appendChild(script)
|
|
177
|
+
|
|
178
|
+
await new Promise<void>((resolve) => {
|
|
179
|
+
script.onload = () => resolve()
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
window.googletag = window.googletag || { cmd: [] }
|
|
184
|
+
window.googletag.cmd.push(() => {
|
|
185
|
+
window.googletag.pubads().enableSingleRequest()
|
|
186
|
+
if (config.enableLazyLoad) {
|
|
187
|
+
window.googletag.pubads().enableLazyLoad({
|
|
188
|
+
fetchMarginPercent: 100,
|
|
189
|
+
renderMarginPercent: config.lazyLoadOffset || 50,
|
|
190
|
+
mobileScaling: 2.0,
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
window.googletag.enableServices()
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
setIsReady(true)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
initNetwork()
|
|
201
|
+
}, [config])
|
|
202
|
+
|
|
203
|
+
// Check for existing consent
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
if (config.enableConsent && typeof window !== "undefined") {
|
|
206
|
+
// Check for TCF v2 consent
|
|
207
|
+
const storedConsent = localStorage.getItem("ad_consent")
|
|
208
|
+
if (storedConsent !== null) {
|
|
209
|
+
setHasConsent(storedConsent === "true")
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
setHasConsent(true) // No consent required
|
|
213
|
+
}
|
|
214
|
+
}, [config.enableConsent])
|
|
215
|
+
|
|
216
|
+
const setConsent = useCallback((consent: boolean) => {
|
|
217
|
+
setHasConsent(consent)
|
|
218
|
+
if (typeof window !== "undefined") {
|
|
219
|
+
localStorage.setItem("ad_consent", String(consent))
|
|
220
|
+
}
|
|
221
|
+
}, [])
|
|
222
|
+
|
|
223
|
+
const registerSlot = useCallback((slot: AdSlot) => {
|
|
224
|
+
setSlots((prev) => {
|
|
225
|
+
const newSlots = new Map(prev)
|
|
226
|
+
newSlots.set(slot.id, slot)
|
|
227
|
+
return newSlots
|
|
228
|
+
})
|
|
229
|
+
}, [])
|
|
230
|
+
|
|
231
|
+
const unregisterSlot = useCallback((slotId: string) => {
|
|
232
|
+
setSlots((prev) => {
|
|
233
|
+
const newSlots = new Map(prev)
|
|
234
|
+
newSlots.delete(slotId)
|
|
235
|
+
return newSlots
|
|
236
|
+
})
|
|
237
|
+
}, [])
|
|
238
|
+
|
|
239
|
+
const refreshSlot = useCallback(
|
|
240
|
+
(slotId: string) => {
|
|
241
|
+
if (config.network === "gpt" && window.googletag) {
|
|
242
|
+
window.googletag.cmd.push(() => {
|
|
243
|
+
window.googletag.pubads().refresh()
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
// For custom network, trigger re-fetch
|
|
247
|
+
},
|
|
248
|
+
[config.network]
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
const refreshAll = useCallback(() => {
|
|
252
|
+
slots.forEach((_, slotId) => refreshSlot(slotId))
|
|
253
|
+
}, [slots, refreshSlot])
|
|
254
|
+
|
|
255
|
+
const getCustomAd = useCallback(
|
|
256
|
+
async (slotId: string): Promise<CustomAd | null> => {
|
|
257
|
+
if (!config.customAdServer) return null
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const response = await fetch(`${config.customAdServer}/ad`, {
|
|
261
|
+
method: "POST",
|
|
262
|
+
headers: {
|
|
263
|
+
"Content-Type": "application/json",
|
|
264
|
+
...(config.customApiKey && { Authorization: `Bearer ${config.customApiKey}` }),
|
|
265
|
+
},
|
|
266
|
+
body: JSON.stringify({
|
|
267
|
+
slotId,
|
|
268
|
+
slot: slots.get(slotId),
|
|
269
|
+
testMode: config.testMode,
|
|
270
|
+
}),
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
if (!response.ok) return null
|
|
274
|
+
return response.json()
|
|
275
|
+
} catch {
|
|
276
|
+
return null
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
[config.customAdServer, config.customApiKey, config.testMode, slots]
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
const trackEvent = useCallback(
|
|
283
|
+
(event: AdEvent) => {
|
|
284
|
+
if (config.debugMode) {
|
|
285
|
+
console.log("[WakaAd]", event.type, event.slotId, event.data)
|
|
286
|
+
}
|
|
287
|
+
onEvent?.(event)
|
|
288
|
+
eventQueueRef.current.push(event)
|
|
289
|
+
},
|
|
290
|
+
[config.debugMode, onEvent]
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
const value: AdContextValue = {
|
|
294
|
+
config,
|
|
295
|
+
isReady,
|
|
296
|
+
hasConsent,
|
|
297
|
+
setConsent,
|
|
298
|
+
registerSlot,
|
|
299
|
+
unregisterSlot,
|
|
300
|
+
refreshSlot,
|
|
301
|
+
refreshAll,
|
|
302
|
+
getCustomAd,
|
|
303
|
+
trackEvent,
|
|
304
|
+
slots,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return <AdContext.Provider value={value}>{children}</AdContext.Provider>
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ==================== Type augmentation for GPT ====================
|
|
311
|
+
|
|
312
|
+
declare global {
|
|
313
|
+
interface Window {
|
|
314
|
+
googletag: {
|
|
315
|
+
cmd: Array<() => void>
|
|
316
|
+
defineSlot: (adUnitPath: string, size: [number, number], divId: string) => unknown
|
|
317
|
+
pubads: () => {
|
|
318
|
+
enableSingleRequest: () => void
|
|
319
|
+
enableLazyLoad: (config: Record<string, unknown>) => void
|
|
320
|
+
refresh: () => void
|
|
321
|
+
setTargeting: (key: string, value: string | string[]) => void
|
|
322
|
+
}
|
|
323
|
+
enableServices: () => void
|
|
324
|
+
display: (divId: string) => void
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export default WakaAdProvider
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { useRef, useState, useEffect } from "react"
|
|
5
|
+
import { cn } from "../../utils/cn"
|
|
6
|
+
import { WakaAdBanner, type WakaAdBannerProps } from "../waka-ad-banner"
|
|
7
|
+
|
|
8
|
+
export interface WakaAdSidebarProps extends Omit<WakaAdBannerProps, "size"> {
|
|
9
|
+
/** Enable sticky behavior */
|
|
10
|
+
sticky?: boolean
|
|
11
|
+
/** Top offset when sticky (in pixels) */
|
|
12
|
+
stickyOffset?: number
|
|
13
|
+
/** Bottom boundary element selector */
|
|
14
|
+
boundarySelector?: string
|
|
15
|
+
/** Gap between multiple ads */
|
|
16
|
+
gap?: number
|
|
17
|
+
/** Additional ads to show below */
|
|
18
|
+
additionalSlots?: Array<{
|
|
19
|
+
slotId: string
|
|
20
|
+
adUnitPath?: string
|
|
21
|
+
}>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function WakaAdSidebar({
|
|
25
|
+
slotId,
|
|
26
|
+
sticky = true,
|
|
27
|
+
stickyOffset = 80,
|
|
28
|
+
boundarySelector,
|
|
29
|
+
gap = 16,
|
|
30
|
+
additionalSlots = [],
|
|
31
|
+
className,
|
|
32
|
+
...bannerProps
|
|
33
|
+
}: WakaAdSidebarProps) {
|
|
34
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
35
|
+
const [isSticky, setIsSticky] = useState(false)
|
|
36
|
+
const [stickyStyle, setStickyStyle] = useState<React.CSSProperties>({})
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!sticky || !containerRef.current) return
|
|
40
|
+
|
|
41
|
+
const container = containerRef.current
|
|
42
|
+
const boundary = boundarySelector ? document.querySelector(boundarySelector) : null
|
|
43
|
+
|
|
44
|
+
const handleScroll = () => {
|
|
45
|
+
const containerRect = container.getBoundingClientRect()
|
|
46
|
+
const boundaryRect = boundary?.getBoundingClientRect()
|
|
47
|
+
|
|
48
|
+
// Check if we should be sticky
|
|
49
|
+
if (containerRect.top <= stickyOffset) {
|
|
50
|
+
// Check if we've hit the boundary
|
|
51
|
+
if (boundaryRect) {
|
|
52
|
+
const maxTop = boundaryRect.top - container.offsetHeight - gap
|
|
53
|
+
if (maxTop < stickyOffset) {
|
|
54
|
+
// Stop at boundary
|
|
55
|
+
setStickyStyle({
|
|
56
|
+
position: "absolute",
|
|
57
|
+
top: boundary!.offsetTop - container.offsetHeight - gap,
|
|
58
|
+
width: containerRect.width,
|
|
59
|
+
})
|
|
60
|
+
setIsSticky(false)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setIsSticky(true)
|
|
66
|
+
setStickyStyle({
|
|
67
|
+
position: "fixed",
|
|
68
|
+
top: stickyOffset,
|
|
69
|
+
width: containerRect.width,
|
|
70
|
+
})
|
|
71
|
+
} else {
|
|
72
|
+
setIsSticky(false)
|
|
73
|
+
setStickyStyle({})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
window.addEventListener("scroll", handleScroll, { passive: true })
|
|
78
|
+
handleScroll() // Initial check
|
|
79
|
+
|
|
80
|
+
return () => window.removeEventListener("scroll", handleScroll)
|
|
81
|
+
}, [sticky, stickyOffset, boundarySelector, gap])
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
ref={containerRef}
|
|
86
|
+
className={cn("relative", className)}
|
|
87
|
+
data-sticky={isSticky}
|
|
88
|
+
>
|
|
89
|
+
<div style={stickyStyle}>
|
|
90
|
+
<div className="flex flex-col" style={{ gap }}>
|
|
91
|
+
<WakaAdBanner
|
|
92
|
+
slotId={slotId}
|
|
93
|
+
size="skyscraper"
|
|
94
|
+
{...bannerProps}
|
|
95
|
+
/>
|
|
96
|
+
|
|
97
|
+
{additionalSlots.map((slot, index) => (
|
|
98
|
+
<WakaAdBanner
|
|
99
|
+
key={slot.slotId}
|
|
100
|
+
slotId={slot.slotId}
|
|
101
|
+
size="rectangle"
|
|
102
|
+
adUnitPath={slot.adUnitPath}
|
|
103
|
+
lazyLoad
|
|
104
|
+
{...bannerProps}
|
|
105
|
+
/>
|
|
106
|
+
))}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default WakaAdSidebar
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { useState, useEffect } from "react"
|
|
5
|
+
import { cn } from "../../utils/cn"
|
|
6
|
+
import { WakaAdBanner, type WakaAdBannerProps } from "../waka-ad-banner"
|
|
7
|
+
import { X } from "lucide-react"
|
|
8
|
+
|
|
9
|
+
export interface WakaAdStickyFooterProps extends Omit<WakaAdBannerProps, "size"> {
|
|
10
|
+
/** Allow user to dismiss */
|
|
11
|
+
dismissable?: boolean
|
|
12
|
+
/** Show close button after delay (seconds) */
|
|
13
|
+
showCloseAfter?: number
|
|
14
|
+
/** Animation style */
|
|
15
|
+
animation?: "slide" | "fade" | "none"
|
|
16
|
+
/** Safe area padding (for mobile notches) */
|
|
17
|
+
safeAreaPadding?: boolean
|
|
18
|
+
/** Background blur */
|
|
19
|
+
blur?: boolean
|
|
20
|
+
/** Container class name */
|
|
21
|
+
containerClassName?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function WakaAdStickyFooter({
|
|
25
|
+
slotId,
|
|
26
|
+
dismissable = true,
|
|
27
|
+
showCloseAfter = 0,
|
|
28
|
+
animation = "slide",
|
|
29
|
+
safeAreaPadding = true,
|
|
30
|
+
blur = true,
|
|
31
|
+
className,
|
|
32
|
+
containerClassName,
|
|
33
|
+
...bannerProps
|
|
34
|
+
}: WakaAdStickyFooterProps) {
|
|
35
|
+
const [isVisible, setIsVisible] = useState(true)
|
|
36
|
+
const [canClose, setCanClose] = useState(showCloseAfter === 0)
|
|
37
|
+
const [isAnimatingOut, setIsAnimatingOut] = useState(false)
|
|
38
|
+
|
|
39
|
+
// Delayed close button
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (showCloseAfter > 0 && !canClose) {
|
|
42
|
+
const timer = setTimeout(() => {
|
|
43
|
+
setCanClose(true)
|
|
44
|
+
}, showCloseAfter * 1000)
|
|
45
|
+
return () => clearTimeout(timer)
|
|
46
|
+
}
|
|
47
|
+
}, [showCloseAfter, canClose])
|
|
48
|
+
|
|
49
|
+
const handleClose = () => {
|
|
50
|
+
if (!dismissable || !canClose) return
|
|
51
|
+
|
|
52
|
+
if (animation !== "none") {
|
|
53
|
+
setIsAnimatingOut(true)
|
|
54
|
+
setTimeout(() => {
|
|
55
|
+
setIsVisible(false)
|
|
56
|
+
}, 300)
|
|
57
|
+
} else {
|
|
58
|
+
setIsVisible(false)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!isVisible) return null
|
|
63
|
+
|
|
64
|
+
const animationClasses = {
|
|
65
|
+
slide: cn(
|
|
66
|
+
"transition-transform duration-300",
|
|
67
|
+
isAnimatingOut ? "translate-y-full" : "translate-y-0"
|
|
68
|
+
),
|
|
69
|
+
fade: cn(
|
|
70
|
+
"transition-opacity duration-300",
|
|
71
|
+
isAnimatingOut ? "opacity-0" : "opacity-100"
|
|
72
|
+
),
|
|
73
|
+
none: "",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
className={cn(
|
|
79
|
+
"fixed bottom-0 left-0 right-0 z-40",
|
|
80
|
+
blur && "backdrop-blur-sm",
|
|
81
|
+
safeAreaPadding && "pb-safe",
|
|
82
|
+
animationClasses[animation],
|
|
83
|
+
containerClassName
|
|
84
|
+
)}
|
|
85
|
+
role="complementary"
|
|
86
|
+
aria-label="Advertisement"
|
|
87
|
+
>
|
|
88
|
+
<div
|
|
89
|
+
className={cn(
|
|
90
|
+
"relative flex items-center justify-center",
|
|
91
|
+
"bg-background/95 border-t border-border shadow-lg",
|
|
92
|
+
"py-2 px-4",
|
|
93
|
+
className
|
|
94
|
+
)}
|
|
95
|
+
>
|
|
96
|
+
{/* Close button */}
|
|
97
|
+
{dismissable && (
|
|
98
|
+
<button
|
|
99
|
+
onClick={handleClose}
|
|
100
|
+
disabled={!canClose}
|
|
101
|
+
className={cn(
|
|
102
|
+
"absolute top-1 right-1 p-1 rounded-full transition-all",
|
|
103
|
+
canClose
|
|
104
|
+
? "bg-muted hover:bg-muted-foreground/20 text-muted-foreground hover:text-foreground cursor-pointer"
|
|
105
|
+
: "bg-muted/50 text-muted-foreground/50 cursor-not-allowed"
|
|
106
|
+
)}
|
|
107
|
+
aria-label="Close advertisement"
|
|
108
|
+
>
|
|
109
|
+
<X className="h-4 w-4" />
|
|
110
|
+
</button>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{/* Ad content */}
|
|
114
|
+
<WakaAdBanner
|
|
115
|
+
slotId={slotId}
|
|
116
|
+
size="mobile-banner"
|
|
117
|
+
showBadge={false}
|
|
118
|
+
{...bannerProps}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export default WakaAdStickyFooter
|