@pagepact/interactive-ad-sdk 1.0.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/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # Interactive Ad SDK
2
+
3
+ A modern, framework-agnostic interactive advertisement web component. It allows you to display beautiful banner ads while enabling users to engage by liking, disliking, viewing, and commenting.
4
+
5
+ ## Features
6
+
7
+ - **Framework Agnostic**: Built with native Web Components, meaning it works in React, Vue, Angular, or Vanilla JS.
8
+ - **Zero Setup**: Connects automatically to the Ad Network's database out of the box. No backend configuration needed from the webmaster.
9
+ - **Modern Aesthetics**: Premium, responsive default styling using Shadow DOM encapsulation.
10
+ - **Engaging UI**: Includes an engagement bar and comment section out-of-the-box.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install interactive-ad-sdk
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ Simply import the SDK into your project's main entry file (e.g. `main.js`, `App.jsx`, or `_app.js`). This registers the `<interactive-ad>` tag.
21
+
22
+ ```javascript
23
+ import 'interactive-ad-sdk';
24
+ ```
25
+
26
+ Then, you can use the `<interactive-ad>` HTML tag anywhere in your application!
27
+
28
+ ```html
29
+ <interactive-ad
30
+ ad-id="campaign-summer-sale"
31
+ title="Summer Cloud Hosting Sale"
32
+ description="Get 50% off all our premium cloud servers for a limited time."
33
+ image-url="https://example.com/banner.jpg"
34
+ link-url="https://example.com/sale"
35
+ ></interactive-ad>
36
+ ```
37
+
38
+ ### Attributes
39
+
40
+ - `ad-id` (Required): A unique string identifier for this advertisement.
41
+ - `variant`: The standard IAB ad size layout. Can be `rectangle` (300x250, default), `leaderboard` (728x90), or `skyscraper` (160x600).
42
+
43
+ ### Wrapping 3rd-Party Ads (like Google AdSense)
44
+
45
+ Instead of passing static attributes, you drop your ad creative (like a Google Ads `<ins>` and `<script>` tag, or an `<img>` tag) directly **inside** the `<interactive-ad>` tag.
46
+
47
+ ```html
48
+ <interactive-ad variant="rectangle" ad-id="my-campaign-123">
49
+ <!-- Your Ad Network Code goes here -->
50
+ <ins class="adsbygoogle"
51
+ style="display:inline-block;width:300px;height:250px"
52
+ data-ad-client="ca-pub-12345"
53
+ data-ad-slot="67890"></ins>
54
+ <script>
55
+ (adsbygoogle = window.adsbygoogle || []).push({});
56
+ </script>
57
+ </interactive-ad>
58
+ ```
59
+
60
+ ## License
61
+
62
+ MIT
@@ -0,0 +1,396 @@
1
+ import { initializeApp as e } from "firebase/app";
2
+ import { addDoc as t, collection as n, doc as r, getDoc as i, getFirestore as a, onSnapshot as o, orderBy as s, query as c, setDoc as l } from "firebase/firestore";
3
+ //#region src/firebase-config.js
4
+ var u = a(e({
5
+ apiKey: "AIzaSyBktbQVYy7PM993JtJ9rORlyFd5KhPyotw",
6
+ authDomain: "ad-sdk-backend.firebaseapp.com",
7
+ projectId: "ad-sdk-backend",
8
+ storageBucket: "ad-sdk-backend.firebasestorage.app",
9
+ messagingSenderId: "1096291583467",
10
+ appId: "1:1096291583467:web:5fdf82d12c72502bda45ae",
11
+ measurementId: "G-N1X2CJRW07"
12
+ })), d = () => u, f = class extends HTMLElement {
13
+ constructor() {
14
+ super(), this.attachShadow({ mode: "open" }), this.adId = null, this.likes = 0, this.dislikes = 0, this.views = 0, this.userVote = null, this.comments = [], this.commentsVisible = !1, this.username = this.initUsername(), this.unsubscribeAd = null, this.unsubscribeComments = null;
15
+ }
16
+ initUsername() {
17
+ let e = localStorage.getItem("ad_sdk_username");
18
+ if (!e) {
19
+ let t = [
20
+ "Red",
21
+ "Blue",
22
+ "Green",
23
+ "Yellow",
24
+ "Purple",
25
+ "Orange",
26
+ "Silver",
27
+ "Golden",
28
+ "Crimson",
29
+ "Azure"
30
+ ], n = [
31
+ "Panda",
32
+ "Tiger",
33
+ "Eagle",
34
+ "Dolphin",
35
+ "Fox",
36
+ "Wolf",
37
+ "Bear",
38
+ "Falcon",
39
+ "Panther",
40
+ "Lion"
41
+ ];
42
+ e = `${t[Math.floor(Math.random() * t.length)]}${n[Math.floor(Math.random() * n.length)]}${Math.floor(Math.random() * 999) + 1}`, localStorage.setItem("ad_sdk_username", e);
43
+ }
44
+ return e;
45
+ }
46
+ static get observedAttributes() {
47
+ return ["ad-id", "variant"];
48
+ }
49
+ attributeChangedCallback(e, t, n) {
50
+ e === "ad-id" && (this.adId = n, this.userVote = sessionStorage.getItem(`ad_vote_${this.adId}`), this.initFirebaseListeners()), this.render();
51
+ }
52
+ connectedCallback() {
53
+ this.render();
54
+ }
55
+ disconnectedCallback() {
56
+ this.unsubscribeAd && this.unsubscribeAd(), this.unsubscribeComments && this.unsubscribeComments();
57
+ }
58
+ async initFirebaseListeners() {
59
+ let e = d();
60
+ if (!e || !this.adId) return;
61
+ let t = r(e, "ads", this.adId), a = await i(t);
62
+ a.exists() ? await l(t, { views: (a.data().views || 0) + 1 }, { merge: !0 }) : await l(t, {
63
+ likes: 0,
64
+ dislikes: 0,
65
+ views: 1
66
+ }), this.unsubscribeAd = o(t, (e) => {
67
+ let t = e.data();
68
+ t && (this.likes = t.likes || 0, this.dislikes = t.dislikes || 0, this.views = t.views || 0, this.updateEngagementUI());
69
+ });
70
+ let u = c(n(e, "ads", this.adId, "comments"), s("createdAt", "desc"));
71
+ this.unsubscribeComments = o(u, (e) => {
72
+ this.comments = e.docs.map((e) => ({
73
+ id: e.id,
74
+ ...e.data()
75
+ })), this.updateCommentsUI();
76
+ });
77
+ }
78
+ async handleLike() {
79
+ if (this.userVote) return;
80
+ let e = d();
81
+ if (!e || !this.adId) return;
82
+ let t = r(e, "ads", this.adId);
83
+ this.userVote = "like", sessionStorage.setItem(`ad_vote_${this.adId}`, "like"), this.updateEngagementUI(), await l(t, { likes: this.likes + 1 }, { merge: !0 });
84
+ }
85
+ async handleDislike() {
86
+ if (this.userVote) return;
87
+ let e = d();
88
+ if (!e || !this.adId) return;
89
+ let t = r(e, "ads", this.adId);
90
+ this.userVote = "dislike", sessionStorage.setItem(`ad_vote_${this.adId}`, "dislike"), this.updateEngagementUI(), await l(t, { dislikes: this.dislikes + 1 }, { merge: !0 });
91
+ }
92
+ async handleAddComment(e) {
93
+ e.preventDefault();
94
+ let r = d();
95
+ if (!r || !this.adId) return;
96
+ let i = this.shadowRoot.querySelector(".comment-input"), a = i.value.trim();
97
+ a && (await t(n(r, "ads", this.adId, "comments"), {
98
+ text: a,
99
+ username: this.username,
100
+ createdAt: /* @__PURE__ */ new Date()
101
+ }), i.value = "");
102
+ }
103
+ render() {
104
+ let e = this.getAttribute("variant") || "rectangle";
105
+ this.shadowRoot.innerHTML = `
106
+ <style>
107
+ :host {
108
+ display: block;
109
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
110
+ background: #ffffff;
111
+ border-radius: 8px;
112
+ box-shadow: 0 4px 15px rgba(0,0,0,0.08);
113
+ overflow: hidden;
114
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
115
+ border: 1px solid rgba(0,0,0,0.05);
116
+ width: 100%;
117
+ }
118
+ :host(:hover) {
119
+ transform: translateY(-2px);
120
+ box-shadow: 0 8px 25px rgba(0,0,0,0.12);
121
+ }
122
+ .ad-container {
123
+ display: flex;
124
+ flex-direction: column;
125
+ }
126
+
127
+ .ad-creative {
128
+ flex-shrink: 0;
129
+ display: block;
130
+ width: 100%;
131
+ overflow: hidden;
132
+ }
133
+ ::slotted(*) {
134
+ display: block;
135
+ width: 100%;
136
+ height: 100%;
137
+ object-fit: cover;
138
+ }
139
+ .ad-content {
140
+ padding: 12px;
141
+ display: flex;
142
+ flex-direction: column;
143
+ }
144
+
145
+ /* Rectangle Variant (300x250) */
146
+ .variant-rectangle {
147
+ max-width: 300px;
148
+ margin: 0 auto;
149
+ }
150
+ .variant-rectangle .ad-creative {
151
+ aspect-ratio: 300 / 250;
152
+ }
153
+ .variant-rectangle .hide-mobile {
154
+ display: none;
155
+ }
156
+
157
+ /* Leaderboard Variant (728x90) */
158
+ .variant-leaderboard {
159
+ max-width: 728px;
160
+ margin: 0 auto;
161
+ }
162
+ .variant-leaderboard .ad-creative {
163
+ aspect-ratio: 728 / 90;
164
+ }
165
+ .variant-leaderboard .ad-content {
166
+ padding: 16px;
167
+ }
168
+
169
+ /* Skyscraper Variant (160x600) */
170
+ .variant-skyscraper {
171
+ max-width: 160px;
172
+ margin: 0 auto;
173
+ }
174
+ .variant-skyscraper .ad-creative {
175
+ aspect-ratio: 160 / 600;
176
+ }
177
+ .variant-skyscraper .engagement-bar {
178
+ flex-wrap: wrap;
179
+ justify-content: center;
180
+ }
181
+ .variant-skyscraper .action-btn {
182
+ width: 100%;
183
+ justify-content: center;
184
+ }
185
+ .variant-skyscraper .views-count {
186
+ width: 100%;
187
+ text-align: center;
188
+ margin-top: 8px;
189
+ }
190
+
191
+ /* Mobile Responsiveness */
192
+ @media (max-width: 480px) {
193
+ .engagement-bar {
194
+ gap: 6px;
195
+ }
196
+ .action-btn {
197
+ padding: 6px 10px;
198
+ font-size: 0.85rem;
199
+ }
200
+ .hide-mobile {
201
+ display: none;
202
+ }
203
+ }
204
+ .engagement-bar {
205
+ display: flex;
206
+ align-items: center;
207
+ gap: 12px;
208
+ padding-top: 16px;
209
+ border-top: 1px solid #f0f0f5;
210
+ }
211
+ .action-btn {
212
+ display: flex;
213
+ align-items: center;
214
+ gap: 6px;
215
+ background: #f8f9fc;
216
+ border: 1px solid #e2e8f0;
217
+ border-radius: 20px;
218
+ padding: 8px 16px;
219
+ font-size: 0.9rem;
220
+ font-weight: 600;
221
+ color: #4a5568;
222
+ cursor: pointer;
223
+ transition: all 0.2s;
224
+ }
225
+ .action-btn:hover {
226
+ background: #edf2f7;
227
+ color: #2d3748;
228
+ }
229
+ .action-btn.like:hover {
230
+ color: #3182ce;
231
+ border-color: #bee3f8;
232
+ background: #ebf8ff;
233
+ }
234
+ .action-btn.dislike:hover {
235
+ color: #e53e3e;
236
+ border-color: #fed7d7;
237
+ background: #fff5f5;
238
+ }
239
+ .action-btn.voted {
240
+ opacity: 0.5;
241
+ cursor: not-allowed;
242
+ }
243
+ .action-btn.active.like {
244
+ color: #3182ce;
245
+ border-color: #bee3f8;
246
+ background: #ebf8ff;
247
+ opacity: 1;
248
+ }
249
+ .action-btn.active.dislike {
250
+ color: #e53e3e;
251
+ border-color: #fed7d7;
252
+ background: #fff5f5;
253
+ opacity: 1;
254
+ }
255
+ .views-count {
256
+ margin-left: auto;
257
+ font-size: 0.85rem;
258
+ color: #a0aec0;
259
+ font-weight: 500;
260
+ white-space: nowrap;
261
+ }
262
+ .comments-section {
263
+ padding: 16px 20px;
264
+ background: #fcfcfd;
265
+ border-top: 1px solid #f0f0f5;
266
+ display: none;
267
+ }
268
+ .comments-section.visible {
269
+ display: block;
270
+ }
271
+ .comment-form {
272
+ display: flex;
273
+ gap: 10px;
274
+ margin-bottom: 16px;
275
+ }
276
+ .comment-input {
277
+ flex: 1;
278
+ padding: 10px 14px;
279
+ border: 1px solid #e2e8f0;
280
+ border-radius: 8px;
281
+ font-size: 0.9rem;
282
+ outline: none;
283
+ transition: border-color 0.2s;
284
+ }
285
+ .comment-input:focus {
286
+ border-color: #4299e1;
287
+ box-shadow: 0 0 0 2px rgba(66, 153, 225, 0.2);
288
+ }
289
+ .submit-btn {
290
+ background: #4299e1;
291
+ color: white;
292
+ border: none;
293
+ border-radius: 8px;
294
+ padding: 0 16px;
295
+ font-weight: 600;
296
+ cursor: pointer;
297
+ transition: background 0.2s;
298
+ }
299
+ .submit-btn:hover {
300
+ background: #3182ce;
301
+ }
302
+ .comment-list {
303
+ display: flex;
304
+ flex-direction: column;
305
+ gap: 12px;
306
+ max-height: 200px;
307
+ overflow-y: auto;
308
+ }
309
+ .comment-item {
310
+ background: white;
311
+ padding: 12px;
312
+ border-radius: 8px;
313
+ border: 1px solid #edf2f7;
314
+ font-size: 0.9rem;
315
+ color: #2d3748;
316
+ }
317
+ .comment-header {
318
+ display: flex;
319
+ justify-content: space-between;
320
+ align-items: baseline;
321
+ margin-bottom: 6px;
322
+ }
323
+ .comment-author {
324
+ font-weight: 600;
325
+ color: #1a202c;
326
+ }
327
+ .comment-time {
328
+ font-size: 0.75rem;
329
+ color: #a0aec0;
330
+ }
331
+ .comment-text {
332
+ line-height: 1.4;
333
+ }
334
+ </style>
335
+
336
+ <div class="variant-${e}">
337
+ <div class="ad-container">
338
+ <div class="ad-creative">
339
+ <slot></slot>
340
+ </div>
341
+ <div class="ad-content">
342
+ <div class="engagement-bar">
343
+ <button class="action-btn like" id="like-btn">
344
+ 👍 <span id="like-count">${this.likes}</span>
345
+ </button>
346
+ <button class="action-btn dislike" id="dislike-btn">
347
+ 👎 <span id="dislike-count">${this.dislikes}</span>
348
+ </button>
349
+ <button class="action-btn" id="toggle-comments-btn">
350
+ 💬 <span class="hide-mobile">Comments</span>
351
+ </button>
352
+ <div class="views-count">
353
+ 👁️ <span id="view-count">${this.views}</span><span class="hide-mobile"> views</span>
354
+ </div>
355
+ </div>
356
+ <div class="comments-section ${this.commentsVisible ? "visible" : ""}" id="comments-section">
357
+ <form class="comment-form" id="comment-form">
358
+ <input type="text" class="comment-input" placeholder="Add a comment..." required />
359
+ <button type="submit" class="submit-btn">Post</button>
360
+ </form>
361
+ <div class="comment-list" id="comment-list">
362
+ <!-- Comments will be rendered here -->
363
+ </div>
364
+ </div>
365
+ </div>
366
+ `, this.shadowRoot.getElementById("like-btn").addEventListener("click", () => this.handleLike()), this.shadowRoot.getElementById("dislike-btn").addEventListener("click", () => this.handleDislike()), this.shadowRoot.getElementById("toggle-comments-btn").addEventListener("click", () => this.toggleComments()), this.shadowRoot.getElementById("comment-form").addEventListener("submit", (e) => this.handleAddComment(e)), this.updateCommentsUI();
367
+ }
368
+ toggleComments() {
369
+ this.commentsVisible = !this.commentsVisible;
370
+ let e = this.shadowRoot.getElementById("comments-section");
371
+ e && (this.commentsVisible ? e.classList.add("visible") : e.classList.remove("visible"));
372
+ }
373
+ updateEngagementUI() {
374
+ if (!this.shadowRoot) return;
375
+ let e = this.shadowRoot.getElementById("like-btn"), t = this.shadowRoot.getElementById("dislike-btn"), n = this.shadowRoot.getElementById("like-count"), r = this.shadowRoot.getElementById("dislike-count"), i = this.shadowRoot.getElementById("view-count");
376
+ n && (n.textContent = this.likes), r && (r.textContent = this.dislikes), i && (i.textContent = this.views), this.userVote && e && t && (e.classList.add("voted"), t.classList.add("voted"), this.userVote === "like" && e.classList.add("active"), this.userVote === "dislike" && t.classList.add("active"));
377
+ }
378
+ updateCommentsUI() {
379
+ if (!this.shadowRoot) return;
380
+ let e = this.shadowRoot.getElementById("comment-list");
381
+ e && (e.innerHTML = this.comments.map((e) => `
382
+ <div class="comment-item">
383
+ <div class="comment-header">
384
+ <span class="comment-author">${this.escapeHtml(e.username || "Anonymous")}</span>
385
+ <span class="comment-time">${e.createdAt ? new Date(e.createdAt.toDate ? e.createdAt.toDate() : e.createdAt).toLocaleString() : "Just now"}</span>
386
+ </div>
387
+ <div class="comment-text">${this.escapeHtml(e.text)}</div>
388
+ </div>
389
+ `).join(""));
390
+ }
391
+ escapeHtml(e) {
392
+ return e.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
393
+ }
394
+ };
395
+ customElements.define("interactive-ad", f);
396
+ //#endregion
@@ -0,0 +1,270 @@
1
+ (function(e,t){typeof exports==`object`&&typeof module<`u`?t(require("firebase/app"),require("firebase/firestore")):typeof define==`function`&&define.amd?define([`firebase/app`,`firebase/firestore`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.firebase,e.firebase.firestore))})(this,function(e,t){var n=(0,t.getFirestore)((0,e.initializeApp)({apiKey:`AIzaSyBktbQVYy7PM993JtJ9rORlyFd5KhPyotw`,authDomain:`ad-sdk-backend.firebaseapp.com`,projectId:`ad-sdk-backend`,storageBucket:`ad-sdk-backend.firebasestorage.app`,messagingSenderId:`1096291583467`,appId:`1:1096291583467:web:5fdf82d12c72502bda45ae`,measurementId:`G-N1X2CJRW07`})),r=()=>n,i=class extends HTMLElement{constructor(){super(),this.attachShadow({mode:`open`}),this.adId=null,this.likes=0,this.dislikes=0,this.views=0,this.userVote=null,this.comments=[],this.commentsVisible=!1,this.username=this.initUsername(),this.unsubscribeAd=null,this.unsubscribeComments=null}initUsername(){let e=localStorage.getItem(`ad_sdk_username`);if(!e){let t=[`Red`,`Blue`,`Green`,`Yellow`,`Purple`,`Orange`,`Silver`,`Golden`,`Crimson`,`Azure`],n=[`Panda`,`Tiger`,`Eagle`,`Dolphin`,`Fox`,`Wolf`,`Bear`,`Falcon`,`Panther`,`Lion`];e=`${t[Math.floor(Math.random()*t.length)]}${n[Math.floor(Math.random()*n.length)]}${Math.floor(Math.random()*999)+1}`,localStorage.setItem(`ad_sdk_username`,e)}return e}static get observedAttributes(){return[`ad-id`,`variant`]}attributeChangedCallback(e,t,n){e===`ad-id`&&(this.adId=n,this.userVote=sessionStorage.getItem(`ad_vote_${this.adId}`),this.initFirebaseListeners()),this.render()}connectedCallback(){this.render()}disconnectedCallback(){this.unsubscribeAd&&this.unsubscribeAd(),this.unsubscribeComments&&this.unsubscribeComments()}async initFirebaseListeners(){let e=r();if(!e||!this.adId)return;let n=(0,t.doc)(e,`ads`,this.adId),i=await(0,t.getDoc)(n);i.exists()?await(0,t.setDoc)(n,{views:(i.data().views||0)+1},{merge:!0}):await(0,t.setDoc)(n,{likes:0,dislikes:0,views:1}),this.unsubscribeAd=(0,t.onSnapshot)(n,e=>{let t=e.data();t&&(this.likes=t.likes||0,this.dislikes=t.dislikes||0,this.views=t.views||0,this.updateEngagementUI())});let a=(0,t.query)((0,t.collection)(e,`ads`,this.adId,`comments`),(0,t.orderBy)(`createdAt`,`desc`));this.unsubscribeComments=(0,t.onSnapshot)(a,e=>{this.comments=e.docs.map(e=>({id:e.id,...e.data()})),this.updateCommentsUI()})}async handleLike(){if(this.userVote)return;let e=r();if(!e||!this.adId)return;let n=(0,t.doc)(e,`ads`,this.adId);this.userVote=`like`,sessionStorage.setItem(`ad_vote_${this.adId}`,`like`),this.updateEngagementUI(),await(0,t.setDoc)(n,{likes:this.likes+1},{merge:!0})}async handleDislike(){if(this.userVote)return;let e=r();if(!e||!this.adId)return;let n=(0,t.doc)(e,`ads`,this.adId);this.userVote=`dislike`,sessionStorage.setItem(`ad_vote_${this.adId}`,`dislike`),this.updateEngagementUI(),await(0,t.setDoc)(n,{dislikes:this.dislikes+1},{merge:!0})}async handleAddComment(e){e.preventDefault();let n=r();if(!n||!this.adId)return;let i=this.shadowRoot.querySelector(`.comment-input`),a=i.value.trim();a&&(await(0,t.addDoc)((0,t.collection)(n,`ads`,this.adId,`comments`),{text:a,username:this.username,createdAt:new Date}),i.value=``)}render(){let e=this.getAttribute(`variant`)||`rectangle`;this.shadowRoot.innerHTML=`
2
+ <style>
3
+ :host {
4
+ display: block;
5
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
6
+ background: #ffffff;
7
+ border-radius: 8px;
8
+ box-shadow: 0 4px 15px rgba(0,0,0,0.08);
9
+ overflow: hidden;
10
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
11
+ border: 1px solid rgba(0,0,0,0.05);
12
+ width: 100%;
13
+ }
14
+ :host(:hover) {
15
+ transform: translateY(-2px);
16
+ box-shadow: 0 8px 25px rgba(0,0,0,0.12);
17
+ }
18
+ .ad-container {
19
+ display: flex;
20
+ flex-direction: column;
21
+ }
22
+
23
+ .ad-creative {
24
+ flex-shrink: 0;
25
+ display: block;
26
+ width: 100%;
27
+ overflow: hidden;
28
+ }
29
+ ::slotted(*) {
30
+ display: block;
31
+ width: 100%;
32
+ height: 100%;
33
+ object-fit: cover;
34
+ }
35
+ .ad-content {
36
+ padding: 12px;
37
+ display: flex;
38
+ flex-direction: column;
39
+ }
40
+
41
+ /* Rectangle Variant (300x250) */
42
+ .variant-rectangle {
43
+ max-width: 300px;
44
+ margin: 0 auto;
45
+ }
46
+ .variant-rectangle .ad-creative {
47
+ aspect-ratio: 300 / 250;
48
+ }
49
+ .variant-rectangle .hide-mobile {
50
+ display: none;
51
+ }
52
+
53
+ /* Leaderboard Variant (728x90) */
54
+ .variant-leaderboard {
55
+ max-width: 728px;
56
+ margin: 0 auto;
57
+ }
58
+ .variant-leaderboard .ad-creative {
59
+ aspect-ratio: 728 / 90;
60
+ }
61
+ .variant-leaderboard .ad-content {
62
+ padding: 16px;
63
+ }
64
+
65
+ /* Skyscraper Variant (160x600) */
66
+ .variant-skyscraper {
67
+ max-width: 160px;
68
+ margin: 0 auto;
69
+ }
70
+ .variant-skyscraper .ad-creative {
71
+ aspect-ratio: 160 / 600;
72
+ }
73
+ .variant-skyscraper .engagement-bar {
74
+ flex-wrap: wrap;
75
+ justify-content: center;
76
+ }
77
+ .variant-skyscraper .action-btn {
78
+ width: 100%;
79
+ justify-content: center;
80
+ }
81
+ .variant-skyscraper .views-count {
82
+ width: 100%;
83
+ text-align: center;
84
+ margin-top: 8px;
85
+ }
86
+
87
+ /* Mobile Responsiveness */
88
+ @media (max-width: 480px) {
89
+ .engagement-bar {
90
+ gap: 6px;
91
+ }
92
+ .action-btn {
93
+ padding: 6px 10px;
94
+ font-size: 0.85rem;
95
+ }
96
+ .hide-mobile {
97
+ display: none;
98
+ }
99
+ }
100
+ .engagement-bar {
101
+ display: flex;
102
+ align-items: center;
103
+ gap: 12px;
104
+ padding-top: 16px;
105
+ border-top: 1px solid #f0f0f5;
106
+ }
107
+ .action-btn {
108
+ display: flex;
109
+ align-items: center;
110
+ gap: 6px;
111
+ background: #f8f9fc;
112
+ border: 1px solid #e2e8f0;
113
+ border-radius: 20px;
114
+ padding: 8px 16px;
115
+ font-size: 0.9rem;
116
+ font-weight: 600;
117
+ color: #4a5568;
118
+ cursor: pointer;
119
+ transition: all 0.2s;
120
+ }
121
+ .action-btn:hover {
122
+ background: #edf2f7;
123
+ color: #2d3748;
124
+ }
125
+ .action-btn.like:hover {
126
+ color: #3182ce;
127
+ border-color: #bee3f8;
128
+ background: #ebf8ff;
129
+ }
130
+ .action-btn.dislike:hover {
131
+ color: #e53e3e;
132
+ border-color: #fed7d7;
133
+ background: #fff5f5;
134
+ }
135
+ .action-btn.voted {
136
+ opacity: 0.5;
137
+ cursor: not-allowed;
138
+ }
139
+ .action-btn.active.like {
140
+ color: #3182ce;
141
+ border-color: #bee3f8;
142
+ background: #ebf8ff;
143
+ opacity: 1;
144
+ }
145
+ .action-btn.active.dislike {
146
+ color: #e53e3e;
147
+ border-color: #fed7d7;
148
+ background: #fff5f5;
149
+ opacity: 1;
150
+ }
151
+ .views-count {
152
+ margin-left: auto;
153
+ font-size: 0.85rem;
154
+ color: #a0aec0;
155
+ font-weight: 500;
156
+ white-space: nowrap;
157
+ }
158
+ .comments-section {
159
+ padding: 16px 20px;
160
+ background: #fcfcfd;
161
+ border-top: 1px solid #f0f0f5;
162
+ display: none;
163
+ }
164
+ .comments-section.visible {
165
+ display: block;
166
+ }
167
+ .comment-form {
168
+ display: flex;
169
+ gap: 10px;
170
+ margin-bottom: 16px;
171
+ }
172
+ .comment-input {
173
+ flex: 1;
174
+ padding: 10px 14px;
175
+ border: 1px solid #e2e8f0;
176
+ border-radius: 8px;
177
+ font-size: 0.9rem;
178
+ outline: none;
179
+ transition: border-color 0.2s;
180
+ }
181
+ .comment-input:focus {
182
+ border-color: #4299e1;
183
+ box-shadow: 0 0 0 2px rgba(66, 153, 225, 0.2);
184
+ }
185
+ .submit-btn {
186
+ background: #4299e1;
187
+ color: white;
188
+ border: none;
189
+ border-radius: 8px;
190
+ padding: 0 16px;
191
+ font-weight: 600;
192
+ cursor: pointer;
193
+ transition: background 0.2s;
194
+ }
195
+ .submit-btn:hover {
196
+ background: #3182ce;
197
+ }
198
+ .comment-list {
199
+ display: flex;
200
+ flex-direction: column;
201
+ gap: 12px;
202
+ max-height: 200px;
203
+ overflow-y: auto;
204
+ }
205
+ .comment-item {
206
+ background: white;
207
+ padding: 12px;
208
+ border-radius: 8px;
209
+ border: 1px solid #edf2f7;
210
+ font-size: 0.9rem;
211
+ color: #2d3748;
212
+ }
213
+ .comment-header {
214
+ display: flex;
215
+ justify-content: space-between;
216
+ align-items: baseline;
217
+ margin-bottom: 6px;
218
+ }
219
+ .comment-author {
220
+ font-weight: 600;
221
+ color: #1a202c;
222
+ }
223
+ .comment-time {
224
+ font-size: 0.75rem;
225
+ color: #a0aec0;
226
+ }
227
+ .comment-text {
228
+ line-height: 1.4;
229
+ }
230
+ </style>
231
+
232
+ <div class="variant-${e}">
233
+ <div class="ad-container">
234
+ <div class="ad-creative">
235
+ <slot></slot>
236
+ </div>
237
+ <div class="ad-content">
238
+ <div class="engagement-bar">
239
+ <button class="action-btn like" id="like-btn">
240
+ 👍 <span id="like-count">${this.likes}</span>
241
+ </button>
242
+ <button class="action-btn dislike" id="dislike-btn">
243
+ 👎 <span id="dislike-count">${this.dislikes}</span>
244
+ </button>
245
+ <button class="action-btn" id="toggle-comments-btn">
246
+ 💬 <span class="hide-mobile">Comments</span>
247
+ </button>
248
+ <div class="views-count">
249
+ 👁️ <span id="view-count">${this.views}</span><span class="hide-mobile"> views</span>
250
+ </div>
251
+ </div>
252
+ <div class="comments-section ${this.commentsVisible?`visible`:``}" id="comments-section">
253
+ <form class="comment-form" id="comment-form">
254
+ <input type="text" class="comment-input" placeholder="Add a comment..." required />
255
+ <button type="submit" class="submit-btn">Post</button>
256
+ </form>
257
+ <div class="comment-list" id="comment-list">
258
+ <!-- Comments will be rendered here -->
259
+ </div>
260
+ </div>
261
+ </div>
262
+ `,this.shadowRoot.getElementById(`like-btn`).addEventListener(`click`,()=>this.handleLike()),this.shadowRoot.getElementById(`dislike-btn`).addEventListener(`click`,()=>this.handleDislike()),this.shadowRoot.getElementById(`toggle-comments-btn`).addEventListener(`click`,()=>this.toggleComments()),this.shadowRoot.getElementById(`comment-form`).addEventListener(`submit`,e=>this.handleAddComment(e)),this.updateCommentsUI()}toggleComments(){this.commentsVisible=!this.commentsVisible;let e=this.shadowRoot.getElementById(`comments-section`);e&&(this.commentsVisible?e.classList.add(`visible`):e.classList.remove(`visible`))}updateEngagementUI(){if(!this.shadowRoot)return;let e=this.shadowRoot.getElementById(`like-btn`),t=this.shadowRoot.getElementById(`dislike-btn`),n=this.shadowRoot.getElementById(`like-count`),r=this.shadowRoot.getElementById(`dislike-count`),i=this.shadowRoot.getElementById(`view-count`);n&&(n.textContent=this.likes),r&&(r.textContent=this.dislikes),i&&(i.textContent=this.views),this.userVote&&e&&t&&(e.classList.add(`voted`),t.classList.add(`voted`),this.userVote===`like`&&e.classList.add(`active`),this.userVote===`dislike`&&t.classList.add(`active`))}updateCommentsUI(){if(!this.shadowRoot)return;let e=this.shadowRoot.getElementById(`comment-list`);e&&(e.innerHTML=this.comments.map(e=>`
263
+ <div class="comment-item">
264
+ <div class="comment-header">
265
+ <span class="comment-author">${this.escapeHtml(e.username||`Anonymous`)}</span>
266
+ <span class="comment-time">${e.createdAt?new Date(e.createdAt.toDate?e.createdAt.toDate():e.createdAt).toLocaleString():`Just now`}</span>
267
+ </div>
268
+ <div class="comment-text">${this.escapeHtml(e.text)}</div>
269
+ </div>
270
+ `).join(``))}escapeHtml(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`).replace(/'/g,`&#039;`)}};customElements.define(`interactive-ad`,i)});
@@ -0,0 +1,45 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Interactive Ad SDK Example</title>
7
+ </head>
8
+ <body style="background: #f0f2f5; margin: 0; padding: 40px; font-family: sans-serif;">
9
+
10
+ <h1>Interactive Advertisement SDK Test</h1>
11
+
12
+ <h2 style="color: #4a5568; margin-bottom: 10px;">1. Medium Rectangle (300x250)</h2>
13
+ <interactive-ad variant="rectangle" ad-id="test-campaign-001">
14
+ <!-- Simulating a Google Ad or injected creative -->
15
+ <a href="https://example.com" target="_blank">
16
+ <img src="https://images.unsplash.com/photo-1550751827-4bd374c3f58b?auto=format&fit=crop&w=300&h=250&q=80" alt="Super Fast Cloud Hosting" style="width: 100%; height: 100%; object-fit: cover;" />
17
+ </a>
18
+ </interactive-ad>
19
+
20
+ <br><hr><br>
21
+
22
+ <h2 style="color: #4a5568; margin-bottom: 10px;">2. Leaderboard (728x90)</h2>
23
+ <interactive-ad variant="leaderboard" ad-id="test-campaign-002">
24
+ <a href="https://example.com" target="_blank">
25
+ <img src="https://images.unsplash.com/photo-1504868584819-f8e8b4b6d7e3?auto=format&fit=crop&w=728&h=90&q=80" alt="Summer Sale" style="width: 100%; height: 100%; object-fit: cover;" />
26
+ </a>
27
+ </interactive-ad>
28
+
29
+ <br><hr><br>
30
+
31
+ <h2 style="color: #4a5568; margin-bottom: 10px;">3. Skyscraper (160x600)</h2>
32
+ <interactive-ad variant="skyscraper" ad-id="test-campaign-003">
33
+ <a href="https://example.com" target="_blank">
34
+ <img src="https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=160&h=600&q=80" alt="Premium Support" style="width: 100%; height: 100%; object-fit: cover;" />
35
+ </a>
36
+ </interactive-ad>
37
+
38
+ <!-- 2. Import SDK as a module (No configuration needed by the webmaster!) -->
39
+ <script type="module">
40
+ // In a real app, you would import from the npm package name:
41
+ // import 'interactive-ad-sdk';
42
+ import '../src/index.js';
43
+ </script>
44
+ </body>
45
+ </html>
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@pagepact/interactive-ad-sdk",
3
+ "version": "1.0.0",
4
+ "description": "A framework-agnostic interactive advertisement web component with Firebase integration.",
5
+ "type": "module",
6
+ "main": "./dist/interactive-ad-sdk.umd.cjs",
7
+ "module": "./dist/interactive-ad-sdk.js",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/interactive-ad-sdk.js",
11
+ "require": "./dist/interactive-ad-sdk.umd.cjs"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "dev": "vite",
16
+ "build": "vite build",
17
+ "preview": "vite preview"
18
+ },
19
+ "keywords": [
20
+ "ad",
21
+ "advertisement",
22
+ "web-component",
23
+ "firebase",
24
+ "interactive"
25
+ ],
26
+ "author": "",
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "firebase": "^11.0.2"
30
+ },
31
+ "devDependencies": {
32
+ "vite": "^5.0.0"
33
+ }
34
+ }
@@ -0,0 +1,472 @@
1
+ import { getDb } from './firebase-config';
2
+ import { doc, getDoc, updateDoc, setDoc, collection, addDoc, onSnapshot, query, orderBy } from 'firebase/firestore';
3
+
4
+ class InteractiveAd extends HTMLElement {
5
+ constructor() {
6
+ super();
7
+ this.attachShadow({ mode: 'open' });
8
+ this.adId = null;
9
+ this.likes = 0;
10
+ this.dislikes = 0;
11
+ this.views = 0;
12
+ this.userVote = null; // 'like' or 'dislike'
13
+ this.comments = [];
14
+ this.commentsVisible = false;
15
+ this.username = this.initUsername();
16
+ this.unsubscribeAd = null;
17
+ this.unsubscribeComments = null;
18
+ }
19
+
20
+ initUsername() {
21
+ let name = localStorage.getItem('ad_sdk_username');
22
+ if (!name) {
23
+ const colors = ['Red', 'Blue', 'Green', 'Yellow', 'Purple', 'Orange', 'Silver', 'Golden', 'Crimson', 'Azure'];
24
+ const animals = ['Panda', 'Tiger', 'Eagle', 'Dolphin', 'Fox', 'Wolf', 'Bear', 'Falcon', 'Panther', 'Lion'];
25
+ const color = colors[Math.floor(Math.random() * colors.length)];
26
+ const animal = animals[Math.floor(Math.random() * animals.length)];
27
+ const number = Math.floor(Math.random() * 999) + 1; // 1 to 999
28
+
29
+ name = `${color}${animal}${number}`;
30
+ localStorage.setItem('ad_sdk_username', name);
31
+ }
32
+ return name;
33
+ }
34
+
35
+ static get observedAttributes() {
36
+ return ['ad-id', 'variant'];
37
+ }
38
+
39
+ attributeChangedCallback(name, oldValue, newValue) {
40
+ if (name === 'ad-id') {
41
+ this.adId = newValue;
42
+ this.userVote = sessionStorage.getItem(`ad_vote_${this.adId}`);
43
+ this.initFirebaseListeners();
44
+ }
45
+ // We call render on any attribute change, including 'variant'
46
+ this.render();
47
+ }
48
+
49
+ connectedCallback() {
50
+ this.render();
51
+ }
52
+
53
+ disconnectedCallback() {
54
+ if (this.unsubscribeAd) this.unsubscribeAd();
55
+ if (this.unsubscribeComments) this.unsubscribeComments();
56
+ }
57
+
58
+ async initFirebaseListeners() {
59
+ const db = getDb();
60
+ if (!db || !this.adId) return;
61
+
62
+ const adRef = doc(db, 'ads', this.adId);
63
+
64
+ // Ensure document exists and increment views
65
+ const snap = await getDoc(adRef);
66
+ if (!snap.exists()) {
67
+ await setDoc(adRef, { likes: 0, dislikes: 0, views: 1 });
68
+ } else {
69
+ const currentData = snap.data();
70
+ await setDoc(adRef, { views: (currentData.views || 0) + 1 }, { merge: true });
71
+ }
72
+
73
+ this.unsubscribeAd = onSnapshot(adRef, (doc) => {
74
+ const data = doc.data();
75
+ if (data) {
76
+ this.likes = data.likes || 0;
77
+ this.dislikes = data.dislikes || 0;
78
+ this.views = data.views || 0;
79
+ this.updateEngagementUI();
80
+ }
81
+ });
82
+
83
+ const commentsRef = collection(db, 'ads', this.adId, 'comments');
84
+ const q = query(commentsRef, orderBy('createdAt', 'desc'));
85
+ this.unsubscribeComments = onSnapshot(q, (snapshot) => {
86
+ this.comments = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
87
+ this.updateCommentsUI();
88
+ });
89
+ }
90
+
91
+ async handleLike() {
92
+ if (this.userVote) return; // Prevent multiple votes
93
+
94
+ const db = getDb();
95
+ if (!db || !this.adId) return;
96
+ const adRef = doc(db, 'ads', this.adId);
97
+
98
+ // Optimistically update UI
99
+ this.userVote = 'like';
100
+ sessionStorage.setItem(`ad_vote_${this.adId}`, 'like');
101
+ this.updateEngagementUI();
102
+
103
+ await setDoc(adRef, { likes: this.likes + 1 }, { merge: true });
104
+ }
105
+
106
+ async handleDislike() {
107
+ if (this.userVote) return; // Prevent multiple votes
108
+
109
+ const db = getDb();
110
+ if (!db || !this.adId) return;
111
+ const adRef = doc(db, 'ads', this.adId);
112
+
113
+ // Optimistically update UI
114
+ this.userVote = 'dislike';
115
+ sessionStorage.setItem(`ad_vote_${this.adId}`, 'dislike');
116
+ this.updateEngagementUI();
117
+
118
+ await setDoc(adRef, { dislikes: this.dislikes + 1 }, { merge: true });
119
+ }
120
+
121
+ async handleAddComment(e) {
122
+ e.preventDefault();
123
+ const db = getDb();
124
+ if (!db || !this.adId) return;
125
+
126
+ const input = this.shadowRoot.querySelector('.comment-input');
127
+ const text = input.value.trim();
128
+ if (!text) return;
129
+
130
+ const commentsRef = collection(db, 'ads', this.adId, 'comments');
131
+ await addDoc(commentsRef, {
132
+ text,
133
+ username: this.username,
134
+ createdAt: new Date()
135
+ });
136
+ input.value = '';
137
+ }
138
+
139
+ render() {
140
+ const variant = this.getAttribute('variant') || 'rectangle';
141
+
142
+ this.shadowRoot.innerHTML = `
143
+ <style>
144
+ :host {
145
+ display: block;
146
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
147
+ background: #ffffff;
148
+ border-radius: 8px;
149
+ box-shadow: 0 4px 15px rgba(0,0,0,0.08);
150
+ overflow: hidden;
151
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
152
+ border: 1px solid rgba(0,0,0,0.05);
153
+ width: 100%;
154
+ }
155
+ :host(:hover) {
156
+ transform: translateY(-2px);
157
+ box-shadow: 0 8px 25px rgba(0,0,0,0.12);
158
+ }
159
+ .ad-container {
160
+ display: flex;
161
+ flex-direction: column;
162
+ }
163
+
164
+ .ad-creative {
165
+ flex-shrink: 0;
166
+ display: block;
167
+ width: 100%;
168
+ overflow: hidden;
169
+ }
170
+ ::slotted(*) {
171
+ display: block;
172
+ width: 100%;
173
+ height: 100%;
174
+ object-fit: cover;
175
+ }
176
+ .ad-content {
177
+ padding: 12px;
178
+ display: flex;
179
+ flex-direction: column;
180
+ }
181
+
182
+ /* Rectangle Variant (300x250) */
183
+ .variant-rectangle {
184
+ max-width: 300px;
185
+ margin: 0 auto;
186
+ }
187
+ .variant-rectangle .ad-creative {
188
+ aspect-ratio: 300 / 250;
189
+ }
190
+ .variant-rectangle .hide-mobile {
191
+ display: none;
192
+ }
193
+
194
+ /* Leaderboard Variant (728x90) */
195
+ .variant-leaderboard {
196
+ max-width: 728px;
197
+ margin: 0 auto;
198
+ }
199
+ .variant-leaderboard .ad-creative {
200
+ aspect-ratio: 728 / 90;
201
+ }
202
+ .variant-leaderboard .ad-content {
203
+ padding: 16px;
204
+ }
205
+
206
+ /* Skyscraper Variant (160x600) */
207
+ .variant-skyscraper {
208
+ max-width: 160px;
209
+ margin: 0 auto;
210
+ }
211
+ .variant-skyscraper .ad-creative {
212
+ aspect-ratio: 160 / 600;
213
+ }
214
+ .variant-skyscraper .engagement-bar {
215
+ flex-wrap: wrap;
216
+ justify-content: center;
217
+ }
218
+ .variant-skyscraper .action-btn {
219
+ width: 100%;
220
+ justify-content: center;
221
+ }
222
+ .variant-skyscraper .views-count {
223
+ width: 100%;
224
+ text-align: center;
225
+ margin-top: 8px;
226
+ }
227
+
228
+ /* Mobile Responsiveness */
229
+ @media (max-width: 480px) {
230
+ .engagement-bar {
231
+ gap: 6px;
232
+ }
233
+ .action-btn {
234
+ padding: 6px 10px;
235
+ font-size: 0.85rem;
236
+ }
237
+ .hide-mobile {
238
+ display: none;
239
+ }
240
+ }
241
+ .engagement-bar {
242
+ display: flex;
243
+ align-items: center;
244
+ gap: 12px;
245
+ padding-top: 16px;
246
+ border-top: 1px solid #f0f0f5;
247
+ }
248
+ .action-btn {
249
+ display: flex;
250
+ align-items: center;
251
+ gap: 6px;
252
+ background: #f8f9fc;
253
+ border: 1px solid #e2e8f0;
254
+ border-radius: 20px;
255
+ padding: 8px 16px;
256
+ font-size: 0.9rem;
257
+ font-weight: 600;
258
+ color: #4a5568;
259
+ cursor: pointer;
260
+ transition: all 0.2s;
261
+ }
262
+ .action-btn:hover {
263
+ background: #edf2f7;
264
+ color: #2d3748;
265
+ }
266
+ .action-btn.like:hover {
267
+ color: #3182ce;
268
+ border-color: #bee3f8;
269
+ background: #ebf8ff;
270
+ }
271
+ .action-btn.dislike:hover {
272
+ color: #e53e3e;
273
+ border-color: #fed7d7;
274
+ background: #fff5f5;
275
+ }
276
+ .action-btn.voted {
277
+ opacity: 0.5;
278
+ cursor: not-allowed;
279
+ }
280
+ .action-btn.active.like {
281
+ color: #3182ce;
282
+ border-color: #bee3f8;
283
+ background: #ebf8ff;
284
+ opacity: 1;
285
+ }
286
+ .action-btn.active.dislike {
287
+ color: #e53e3e;
288
+ border-color: #fed7d7;
289
+ background: #fff5f5;
290
+ opacity: 1;
291
+ }
292
+ .views-count {
293
+ margin-left: auto;
294
+ font-size: 0.85rem;
295
+ color: #a0aec0;
296
+ font-weight: 500;
297
+ white-space: nowrap;
298
+ }
299
+ .comments-section {
300
+ padding: 16px 20px;
301
+ background: #fcfcfd;
302
+ border-top: 1px solid #f0f0f5;
303
+ display: none;
304
+ }
305
+ .comments-section.visible {
306
+ display: block;
307
+ }
308
+ .comment-form {
309
+ display: flex;
310
+ gap: 10px;
311
+ margin-bottom: 16px;
312
+ }
313
+ .comment-input {
314
+ flex: 1;
315
+ padding: 10px 14px;
316
+ border: 1px solid #e2e8f0;
317
+ border-radius: 8px;
318
+ font-size: 0.9rem;
319
+ outline: none;
320
+ transition: border-color 0.2s;
321
+ }
322
+ .comment-input:focus {
323
+ border-color: #4299e1;
324
+ box-shadow: 0 0 0 2px rgba(66, 153, 225, 0.2);
325
+ }
326
+ .submit-btn {
327
+ background: #4299e1;
328
+ color: white;
329
+ border: none;
330
+ border-radius: 8px;
331
+ padding: 0 16px;
332
+ font-weight: 600;
333
+ cursor: pointer;
334
+ transition: background 0.2s;
335
+ }
336
+ .submit-btn:hover {
337
+ background: #3182ce;
338
+ }
339
+ .comment-list {
340
+ display: flex;
341
+ flex-direction: column;
342
+ gap: 12px;
343
+ max-height: 200px;
344
+ overflow-y: auto;
345
+ }
346
+ .comment-item {
347
+ background: white;
348
+ padding: 12px;
349
+ border-radius: 8px;
350
+ border: 1px solid #edf2f7;
351
+ font-size: 0.9rem;
352
+ color: #2d3748;
353
+ }
354
+ .comment-header {
355
+ display: flex;
356
+ justify-content: space-between;
357
+ align-items: baseline;
358
+ margin-bottom: 6px;
359
+ }
360
+ .comment-author {
361
+ font-weight: 600;
362
+ color: #1a202c;
363
+ }
364
+ .comment-time {
365
+ font-size: 0.75rem;
366
+ color: #a0aec0;
367
+ }
368
+ .comment-text {
369
+ line-height: 1.4;
370
+ }
371
+ </style>
372
+
373
+ <div class="variant-${variant}">
374
+ <div class="ad-container">
375
+ <div class="ad-creative">
376
+ <slot></slot>
377
+ </div>
378
+ <div class="ad-content">
379
+ <div class="engagement-bar">
380
+ <button class="action-btn like" id="like-btn">
381
+ 👍 <span id="like-count">${this.likes}</span>
382
+ </button>
383
+ <button class="action-btn dislike" id="dislike-btn">
384
+ 👎 <span id="dislike-count">${this.dislikes}</span>
385
+ </button>
386
+ <button class="action-btn" id="toggle-comments-btn">
387
+ 💬 <span class="hide-mobile">Comments</span>
388
+ </button>
389
+ <div class="views-count">
390
+ 👁️ <span id="view-count">${this.views}</span><span class="hide-mobile"> views</span>
391
+ </div>
392
+ </div>
393
+ <div class="comments-section ${this.commentsVisible ? 'visible' : ''}" id="comments-section">
394
+ <form class="comment-form" id="comment-form">
395
+ <input type="text" class="comment-input" placeholder="Add a comment..." required />
396
+ <button type="submit" class="submit-btn">Post</button>
397
+ </form>
398
+ <div class="comment-list" id="comment-list">
399
+ <!-- Comments will be rendered here -->
400
+ </div>
401
+ </div>
402
+ </div>
403
+ `;
404
+
405
+ this.shadowRoot.getElementById('like-btn').addEventListener('click', () => this.handleLike());
406
+ this.shadowRoot.getElementById('dislike-btn').addEventListener('click', () => this.handleDislike());
407
+ this.shadowRoot.getElementById('toggle-comments-btn').addEventListener('click', () => this.toggleComments());
408
+ this.shadowRoot.getElementById('comment-form').addEventListener('submit', (e) => this.handleAddComment(e));
409
+
410
+ this.updateCommentsUI();
411
+ }
412
+
413
+ toggleComments() {
414
+ this.commentsVisible = !this.commentsVisible;
415
+ const section = this.shadowRoot.getElementById('comments-section');
416
+ if (section) {
417
+ if (this.commentsVisible) {
418
+ section.classList.add('visible');
419
+ } else {
420
+ section.classList.remove('visible');
421
+ }
422
+ }
423
+ }
424
+
425
+ updateEngagementUI() {
426
+ if(!this.shadowRoot) return;
427
+
428
+ const likeBtn = this.shadowRoot.getElementById('like-btn');
429
+ const dislikeBtn = this.shadowRoot.getElementById('dislike-btn');
430
+ const likeCount = this.shadowRoot.getElementById('like-count');
431
+ const dislikeCount = this.shadowRoot.getElementById('dislike-count');
432
+ const viewCount = this.shadowRoot.getElementById('view-count');
433
+
434
+ if (likeCount) likeCount.textContent = this.likes;
435
+ if (dislikeCount) dislikeCount.textContent = this.dislikes;
436
+ if (viewCount) viewCount.textContent = this.views;
437
+
438
+ if (this.userVote && likeBtn && dislikeBtn) {
439
+ likeBtn.classList.add('voted');
440
+ dislikeBtn.classList.add('voted');
441
+ if (this.userVote === 'like') likeBtn.classList.add('active');
442
+ if (this.userVote === 'dislike') dislikeBtn.classList.add('active');
443
+ }
444
+ }
445
+
446
+ updateCommentsUI() {
447
+ if(!this.shadowRoot) return;
448
+ const list = this.shadowRoot.getElementById('comment-list');
449
+ if (!list) return;
450
+
451
+ list.innerHTML = this.comments.map(c => `
452
+ <div class="comment-item">
453
+ <div class="comment-header">
454
+ <span class="comment-author">${this.escapeHtml(c.username || 'Anonymous')}</span>
455
+ <span class="comment-time">${c.createdAt ? new Date(c.createdAt.toDate ? c.createdAt.toDate() : c.createdAt).toLocaleString() : 'Just now'}</span>
456
+ </div>
457
+ <div class="comment-text">${this.escapeHtml(c.text)}</div>
458
+ </div>
459
+ `).join('');
460
+ }
461
+
462
+ escapeHtml(unsafe) {
463
+ return unsafe
464
+ .replace(/&/g, "&amp;")
465
+ .replace(/</g, "&lt;")
466
+ .replace(/>/g, "&gt;")
467
+ .replace(/"/g, "&quot;")
468
+ .replace(/'/g, "&#039;");
469
+ }
470
+ }
471
+
472
+ customElements.define('interactive-ad', InteractiveAd);
@@ -0,0 +1,18 @@
1
+ import { initializeApp } from 'firebase/app';
2
+ import { getFirestore } from 'firebase/firestore';
3
+
4
+ // Replace these values with your actual Firebase project config before publishing
5
+ const firebaseConfig = {
6
+ apiKey: "AIzaSyBktbQVYy7PM993JtJ9rORlyFd5KhPyotw",
7
+ authDomain: "ad-sdk-backend.firebaseapp.com",
8
+ projectId: "ad-sdk-backend",
9
+ storageBucket: "ad-sdk-backend.firebasestorage.app",
10
+ messagingSenderId: "1096291583467",
11
+ appId: "1:1096291583467:web:5fdf82d12c72502bda45ae",
12
+ measurementId: "G-N1X2CJRW07"
13
+ };
14
+
15
+ const app = initializeApp(firebaseConfig);
16
+ const db = getFirestore(app);
17
+
18
+ export const getDb = () => db;
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ // Automatically initialize the component when the package is imported
2
+ import './InteractiveAd';
package/vite.config.js ADDED
@@ -0,0 +1,21 @@
1
+ import { defineConfig } from 'vite';
2
+ import { resolve } from 'path';
3
+
4
+ export default defineConfig({
5
+ build: {
6
+ lib: {
7
+ entry: resolve(__dirname, 'src/index.js'),
8
+ name: 'InteractiveAdSDK',
9
+ fileName: 'interactive-ad-sdk',
10
+ },
11
+ rollupOptions: {
12
+ external: ['firebase/app', 'firebase/firestore'],
13
+ output: {
14
+ globals: {
15
+ 'firebase/app': 'firebase',
16
+ 'firebase/firestore': 'firebase.firestore'
17
+ }
18
+ }
19
+ }
20
+ }
21
+ });