adkit-react 0.1.0

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/index.js ADDED
@@ -0,0 +1,412 @@
1
+ "use client";
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/index.ts
32
+ var index_exports = {};
33
+ __export(index_exports, {
34
+ AdSlot: () => AdSlot,
35
+ AdkitProvider: () => AdkitProvider,
36
+ BookingModal: () => BookingModal,
37
+ useAdkit: () => useAdkit
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+
41
+ // src/AdSlot.tsx
42
+ var React3 = __toESM(require("react"));
43
+
44
+ // src/eventClient.ts
45
+ var API_URL = "https://adkit.dev/api/events";
46
+ function sendEvent(payload) {
47
+ try {
48
+ fetch(API_URL, {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json" },
51
+ body: JSON.stringify({ ...payload, timestamp: Date.now() }),
52
+ keepalive: true
53
+ }).catch(() => {
54
+ });
55
+ } catch {
56
+ }
57
+ }
58
+
59
+ // src/AdkitContext.tsx
60
+ var React = __toESM(require("react"));
61
+ var AdkitContext = React.createContext(null);
62
+ function useAdkit() {
63
+ const ctx = React.useContext(AdkitContext);
64
+ if (!ctx) {
65
+ throw new Error(
66
+ '[Adkit] <AdSlot /> must be used inside <AdkitProvider />. Wrap your app with <AdkitProvider siteId="your-site-id">.'
67
+ );
68
+ }
69
+ return ctx;
70
+ }
71
+ function deriveSlotIdentity(siteId, slot) {
72
+ return `${siteId}:${slot}`;
73
+ }
74
+
75
+ // src/BookingModal.tsx
76
+ var React2 = __toESM(require("react"));
77
+ var import_jsx_runtime = require("react/jsx-runtime");
78
+ function BookingModal({ siteId, slot, price, onClose }) {
79
+ const overlayRef = React2.useRef(null);
80
+ React2.useEffect(() => {
81
+ const handleKey = (e) => {
82
+ if (e.key === "Escape") onClose();
83
+ };
84
+ document.addEventListener("keydown", handleKey);
85
+ return () => document.removeEventListener("keydown", handleKey);
86
+ }, [onClose]);
87
+ React2.useEffect(() => {
88
+ const prev = document.body.style.overflow;
89
+ document.body.style.overflow = "hidden";
90
+ return () => {
91
+ document.body.style.overflow = prev;
92
+ };
93
+ }, []);
94
+ const handleOverlayClick = (e) => {
95
+ if (e.target === overlayRef.current) onClose();
96
+ };
97
+ const dollars = price / 100;
98
+ const formatted = Number.isInteger(dollars) ? `$${dollars}` : `$${dollars.toFixed(2)}`;
99
+ const handleBook = () => {
100
+ const params = new URLSearchParams({
101
+ siteId,
102
+ slot,
103
+ price: String(price),
104
+ ref: window.location.href
105
+ });
106
+ window.location.href = `https://adkit.dev/book?${params.toString()}`;
107
+ };
108
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
109
+ "div",
110
+ {
111
+ ref: overlayRef,
112
+ className: "adkit-modal-overlay",
113
+ onClick: handleOverlayClick,
114
+ role: "dialog",
115
+ "aria-modal": "true",
116
+ "aria-label": "Book this ad space",
117
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "adkit-modal-card", children: [
118
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h2", { className: "adkit-modal-headline", children: [
119
+ "Advertise directly on ",
120
+ typeof window !== "undefined" ? window.location.hostname : "this site",
121
+ "."
122
+ ] }),
123
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "adkit-modal-subhead", children: [
124
+ "Rent this ad space for a fixed price and reach your target audience where they really are: ",
125
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: "right here" }),
126
+ "."
127
+ ] }),
128
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("ul", { className: "adkit-modal-bullets", children: [
129
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: "Exclusive placement, no other ads will be shown" }),
130
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: "Fixed price \u2014 no bidding, auctions, or fees" }),
131
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: "See your ad before you pay, no commitments" }),
132
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: "Track your ad's performance on your dashboard" }),
133
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: "Guaranteed to display 24/7 or your money back" })
134
+ ] }),
135
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "adkit-modal-price-section", children: [
136
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "adkit-modal-price", children: [
137
+ formatted,
138
+ " / day"
139
+ ] }),
140
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "adkit-modal-price-helper", children: "Zero commitment. No minimum booking period." })
141
+ ] }),
142
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "adkit-modal-actions", children: [
143
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "adkit-modal-cta", onClick: handleBook, children: "Book this ad" }),
144
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "adkit-modal-redirect-hint", children: "You'll be redirected to Adkit to upload your ad and choose dates." }),
145
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "adkit-modal-cancel", onClick: onClose, children: "Cancel" })
146
+ ] }),
147
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "adkit-modal-footer", children: [
148
+ "Powered by ",
149
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: "https://adkit.dev", target: "_blank", rel: "noopener noreferrer", children: "Adkit" })
150
+ ] })
151
+ ] })
152
+ }
153
+ );
154
+ }
155
+
156
+ // src/AdSlot.tsx
157
+ var import_jsx_runtime2 = require("react/jsx-runtime");
158
+ var RATIO_CSS = {
159
+ "16:9": "16 / 9",
160
+ "4:3": "4 / 3",
161
+ "1:1": "1 / 1",
162
+ "9:16": "9 / 16",
163
+ "banner": "728 / 90"
164
+ };
165
+ var RATIO_VALUE = {
166
+ "16:9": 16 / 9,
167
+ "4:3": 4 / 3,
168
+ "1:1": 1,
169
+ "9:16": 9 / 16,
170
+ "banner": 728 / 90
171
+ };
172
+ var mountedSlots = /* @__PURE__ */ new Set();
173
+ var SERVE_BASE = "https://adkit.dev";
174
+ function useSystemDark() {
175
+ const [isDark, setIsDark] = React3.useState(false);
176
+ React3.useEffect(() => {
177
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
178
+ setIsDark(mq.matches);
179
+ const handler = (e) => setIsDark(e.matches);
180
+ mq.addEventListener("change", handler);
181
+ return () => mq.removeEventListener("change", handler);
182
+ }, []);
183
+ return isDark;
184
+ }
185
+ function AdSlot({
186
+ slot,
187
+ siteId: manualSiteId,
188
+ aspectRatio,
189
+ price = 2500,
190
+ size = "lg",
191
+ theme = "auto",
192
+ className,
193
+ styles,
194
+ silent = false
195
+ }) {
196
+ var _a, _b, _c, _d;
197
+ const slotRef = React3.useRef(null);
198
+ const hasViewedRef = React3.useRef(false);
199
+ const bookingIdRef = React3.useRef(void 0);
200
+ const systemDark = useSystemDark();
201
+ const [servedAd, setServedAd] = React3.useState({ status: "loading" });
202
+ React3.useEffect(() => {
203
+ bookingIdRef.current = servedAd.status === "active" ? servedAd.bookingId : void 0;
204
+ }, [servedAd]);
205
+ if (!aspectRatio) {
206
+ throw new Error("[Adkit] Missing aspectRatio. This prop is required and determines the ad format.");
207
+ }
208
+ if (!/^[A-Za-z0-9_-]+$/.test(slot)) {
209
+ throw new Error(`[Adkit] Invalid slot name "${slot}". Only letters, numbers, hyphens, and underscores allowed.`);
210
+ }
211
+ const ctx = React3.useContext(AdkitContext);
212
+ const siteId = manualSiteId != null ? manualSiteId : ctx == null ? void 0 : ctx.siteId;
213
+ if (!siteId) {
214
+ throw new Error('[Adkit] Missing siteId. Either wrap your app with <AdkitProvider siteId="..."> or pass siteId directly.');
215
+ }
216
+ const slotIdentity = deriveSlotIdentity(siteId, slot);
217
+ const expectedRatio = RATIO_VALUE[aspectRatio];
218
+ React3.useEffect(() => {
219
+ if (!ctx) return;
220
+ const isUnique = ctx.registerSlot(slotIdentity);
221
+ if (!isUnique) {
222
+ console.warn(`[Adkit] Duplicate slot "${slot}" detected on this page.`);
223
+ if (!silent) {
224
+ sendEvent({ type: "slot_duplicate", siteId, slot, pathname: window.location.pathname });
225
+ }
226
+ }
227
+ return () => ctx.unregisterSlot(slotIdentity);
228
+ }, [ctx, siteId, slotIdentity, slot, silent]);
229
+ React3.useEffect(() => {
230
+ if (silent || mountedSlots.has(slotIdentity)) return;
231
+ mountedSlots.add(slotIdentity);
232
+ sendEvent({
233
+ type: "slot_mount",
234
+ siteId,
235
+ slot,
236
+ pathname: typeof window !== "undefined" ? window.location.pathname : "",
237
+ price,
238
+ aspectRatio
239
+ });
240
+ }, [siteId, slotIdentity, slot, silent, price, aspectRatio]);
241
+ React3.useEffect(() => {
242
+ const ac = new AbortController();
243
+ fetch(`${SERVE_BASE}/api/serve?slotId=${encodeURIComponent(slotIdentity)}`, { signal: ac.signal }).then((res) => res.json()).then((data) => {
244
+ if (data && typeof data === "object" && !Array.isArray(data)) {
245
+ const d = data;
246
+ if (d.status === "active" && typeof d.imageUrl === "string" && typeof d.linkUrl === "string") {
247
+ const bookingId = typeof d.bookingId === "string" ? d.bookingId : void 0;
248
+ setServedAd({ status: "active", bookingId, imageUrl: d.imageUrl, linkUrl: d.linkUrl });
249
+ return;
250
+ }
251
+ }
252
+ setServedAd({ status: "empty" });
253
+ }).catch(() => setServedAd({ status: "empty" }));
254
+ return () => ac.abort();
255
+ }, [slotIdentity]);
256
+ React3.useEffect(() => {
257
+ if (silent) return;
258
+ const el = slotRef.current;
259
+ if (!el || typeof IntersectionObserver === "undefined") return;
260
+ const observer = new IntersectionObserver(
261
+ ([entry]) => {
262
+ if (entry.isIntersecting && entry.intersectionRatio >= 0.5 && !hasViewedRef.current) {
263
+ hasViewedRef.current = true;
264
+ const payload = {
265
+ type: "slot_view",
266
+ slotId: slotIdentity,
267
+ pathname: window.location.pathname,
268
+ viewport: { width: window.innerWidth, height: window.innerHeight }
269
+ };
270
+ if (bookingIdRef.current) payload.bookingId = bookingIdRef.current;
271
+ sendEvent(payload);
272
+ observer.disconnect();
273
+ }
274
+ },
275
+ { threshold: [0.5] }
276
+ );
277
+ observer.observe(el);
278
+ return () => observer.disconnect();
279
+ }, [slotIdentity, siteId, slot, silent]);
280
+ React3.useEffect(() => {
281
+ const el = slotRef.current;
282
+ if (!el || typeof ResizeObserver === "undefined") return;
283
+ const checkRatio = () => {
284
+ const { width, height } = el.getBoundingClientRect();
285
+ if (width === 0 || height === 0) return;
286
+ const diff = Math.abs(width / height - expectedRatio) / expectedRatio;
287
+ if (diff > 0.05) {
288
+ console.warn("[Adkit] Slot aspect ratio mismatch. External CSS may be interfering.");
289
+ }
290
+ };
291
+ const observer = new ResizeObserver(checkRatio);
292
+ observer.observe(el);
293
+ checkRatio();
294
+ return () => observer.disconnect();
295
+ }, [expectedRatio]);
296
+ const [modalOpen, setModalOpen] = React3.useState(false);
297
+ const sendSlotClick = (bookingId) => {
298
+ if (typeof window === "undefined") return;
299
+ const payload = {
300
+ type: "slot_click",
301
+ slotId: slotIdentity,
302
+ pathname: window.location.pathname,
303
+ viewport: { width: window.innerWidth, height: window.innerHeight }
304
+ };
305
+ if (bookingId) payload.bookingId = bookingId;
306
+ sendEvent(payload);
307
+ };
308
+ const handlePlaceholderClick = () => {
309
+ if (silent) return;
310
+ sendSlotClick();
311
+ setModalOpen(true);
312
+ };
313
+ const handleAdClick = () => {
314
+ if (!silent && servedAd.status === "active") sendSlotClick(servedAd.bookingId);
315
+ };
316
+ const isDark = theme === "dark" ? true : theme === "light" ? false : systemDark;
317
+ const styleVars = {
318
+ "--adkit-aspect": RATIO_CSS[aspectRatio],
319
+ "--adkit-bg": (_a = styles == null ? void 0 : styles.backgroundColor) != null ? _a : "transparent",
320
+ "--adkit-text-muted": (_b = styles == null ? void 0 : styles.textColorSecondary) != null ? _b : isDark ? "rgba(255,255,255,0.6)" : "rgba(0,0,0,0.6)",
321
+ "--adkit-text": (_c = styles == null ? void 0 : styles.textColorSecondary) != null ? _c : isDark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)",
322
+ "--adkit-text-strong": (_d = styles == null ? void 0 : styles.textColorPrimary) != null ? _d : isDark ? "rgba(255,255,255,0.9)" : "rgba(0,0,0,0.9)",
323
+ "--adkit-border": (styles == null ? void 0 : styles.borderColor) ? `color-mix(in srgb, ${styles.borderColor} 40%, transparent)` : isDark ? "rgba(255,255,255,0.22)" : "rgba(0,0,0,0.1)",
324
+ "--adkit-border-hover": (styles == null ? void 0 : styles.borderColor) ? `color-mix(in srgb, ${styles.borderColor} 60%, transparent)` : isDark ? "rgba(255,255,255,0.38)" : "rgba(0,0,0,0.2)"
325
+ };
326
+ const dollars = price / 100;
327
+ const formattedPrice = Number.isInteger(dollars) ? dollars.toLocaleString() : dollars.toLocaleString(void 0, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
328
+ const isBanner = aspectRatio === "banner";
329
+ const isActive = servedAd.status === "active";
330
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
331
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
332
+ "div",
333
+ {
334
+ ref: slotRef,
335
+ className: `adkit-slot ${className ? className : "adkit-slot--default-width"}`,
336
+ style: styleVars,
337
+ "data-adkit-site": siteId,
338
+ "data-adkit-slot": slot,
339
+ "data-adkit-ratio": aspectRatio,
340
+ "data-adkit-size": size,
341
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "adkit-canvas", children: isActive ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
342
+ "a",
343
+ {
344
+ id: slot,
345
+ href: servedAd.linkUrl,
346
+ target: "_blank",
347
+ rel: "noopener noreferrer",
348
+ onClick: handleAdClick,
349
+ style: { display: "block", width: "100%", height: "100%" },
350
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
351
+ "img",
352
+ {
353
+ src: servedAd.imageUrl,
354
+ alt: "",
355
+ style: { display: "block", width: "100%", height: "100%", objectFit: "contain" }
356
+ }
357
+ )
358
+ }
359
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { id: `${slot}-placeholder`, className: "adkit-box", role: "button", tabIndex: 0, onClick: handlePlaceholderClick, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "adkit-content", children: [
360
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "adkit-label", children: "Your ad here" }),
361
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "adkit-price", children: [
362
+ "$",
363
+ formattedPrice,
364
+ "/day"
365
+ ] }),
366
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "adkit-cta", children: [
367
+ isBanner ? "Rent" : "Rent this spot",
368
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "adkit-arrow", children: "\u2192" })
369
+ ] })
370
+ ] }) }) })
371
+ }
372
+ ),
373
+ !isActive && modalOpen && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
374
+ BookingModal,
375
+ {
376
+ siteId,
377
+ slot,
378
+ price,
379
+ onClose: () => setModalOpen(false)
380
+ }
381
+ )
382
+ ] });
383
+ }
384
+
385
+ // src/AdkitProvider.tsx
386
+ var React4 = __toESM(require("react"));
387
+ var import_jsx_runtime3 = require("react/jsx-runtime");
388
+ function AdkitProvider({ siteId, children }) {
389
+ const slotsRef = React4.useRef(/* @__PURE__ */ new Set());
390
+ const registerSlot = React4.useCallback((identity) => {
391
+ if (slotsRef.current.has(identity)) {
392
+ return false;
393
+ }
394
+ slotsRef.current.add(identity);
395
+ return true;
396
+ }, []);
397
+ const unregisterSlot = React4.useCallback((identity) => {
398
+ slotsRef.current.delete(identity);
399
+ }, []);
400
+ const value = React4.useMemo(
401
+ () => ({ siteId, registerSlot, unregisterSlot }),
402
+ [siteId, registerSlot, unregisterSlot]
403
+ );
404
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AdkitContext.Provider, { value, children });
405
+ }
406
+ // Annotate the CommonJS export names for ESM import in node:
407
+ 0 && (module.exports = {
408
+ AdSlot,
409
+ AdkitProvider,
410
+ BookingModal,
411
+ useAdkit
412
+ });