shared-features 0.0.1
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 +200 -0
- package/dist/AdPanel-D0BiV6Xb.cjs +88 -0
- package/dist/AdPanel-D0BiV6Xb.cjs.map +1 -0
- package/dist/AdPanel-RGRBf4ub.js +89 -0
- package/dist/AdPanel-RGRBf4ub.js.map +1 -0
- package/dist/analytics-6shJHRZG.cjs +463 -0
- package/dist/analytics-6shJHRZG.cjs.map +1 -0
- package/dist/analytics-Bbmodnm_.js +442 -0
- package/dist/analytics-Bbmodnm_.js.map +1 -0
- package/dist/components/ads/AdPanel.d.ts +12 -0
- package/dist/components/ads/AdPanel.d.ts.map +1 -0
- package/dist/components/ads/index.d.ts +9 -0
- package/dist/components/ads/index.d.ts.map +1 -0
- package/dist/components/index.cjs +5 -0
- package/dist/components/index.cjs.map +1 -0
- package/dist/components/index.d.ts +9 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +5 -0
- package/dist/components/index.js.map +1 -0
- package/dist/firebase/config.d.ts +77 -0
- package/dist/firebase/config.d.ts.map +1 -0
- package/dist/firebase/init.d.ts +55 -0
- package/dist/firebase/init.d.ts.map +1 -0
- package/dist/hooks/index.cjs +6 -0
- package/dist/hooks/index.cjs.map +1 -0
- package/dist/hooks/index.d.ts +10 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/useCampaigns.d.ts +59 -0
- package/dist/hooks/useCampaigns.d.ts.map +1 -0
- package/dist/index.cjs +37 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/services/analytics.d.ts +32 -0
- package/dist/services/analytics.d.ts.map +1 -0
- package/dist/services/campaigns.d.ts +31 -0
- package/dist/services/campaigns.d.ts.map +1 -0
- package/dist/services/index.cjs +18 -0
- package/dist/services/index.cjs.map +1 -0
- package/dist/services/index.d.ts +10 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +18 -0
- package/dist/services/index.js.map +1 -0
- package/dist/types/campaigns.d.ts +378 -0
- package/dist/types/campaigns.d.ts.map +1 -0
- package/dist/types/index.cjs +49 -0
- package/dist/types/index.cjs.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +49 -0
- package/dist/types/index.js.map +1 -0
- package/dist/useCampaigns-3NxODLLs.js +98 -0
- package/dist/useCampaigns-3NxODLLs.js.map +1 -0
- package/dist/useCampaigns-BNOHpETm.cjs +97 -0
- package/dist/useCampaigns-BNOHpETm.cjs.map +1 -0
- package/package.json +90 -0
package/README.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# shared-features
|
|
2
|
+
|
|
3
|
+
Centralized common features for Zaions projects. Manage ads, contacts, feature requests, and more from a single admin panel at [aoneahsan.com](https://aoneahsan.com).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Advertising Campaigns** - Cross-promote Zaions products across all projects
|
|
8
|
+
- **Products Catalog** - Centralized product information
|
|
9
|
+
- **Contact Forms** - (Coming soon)
|
|
10
|
+
- **Feature Requests** - (Coming soon)
|
|
11
|
+
- **Payment Options** - (Coming soon)
|
|
12
|
+
- **Social Links** - (Coming soon)
|
|
13
|
+
- **Developer Info** - (Coming soon)
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
yarn add shared-features
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Peer Dependencies
|
|
22
|
+
|
|
23
|
+
This package requires the following peer dependencies:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
yarn add react react-dom firebase @radix-ui/themes zustand
|
|
27
|
+
# Optional for mobile:
|
|
28
|
+
yarn add @capacitor/preferences
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Setup
|
|
32
|
+
|
|
33
|
+
### 1. Initialize the package
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// src/config/shared-features.ts
|
|
37
|
+
import { initSharedFeatures } from 'shared-features';
|
|
38
|
+
|
|
39
|
+
export const sharedFeatures = initSharedFeatures({
|
|
40
|
+
firebaseConfig: {
|
|
41
|
+
apiKey: import.meta.env.VITE_ZAIONS_FIREBASE_API_KEY,
|
|
42
|
+
authDomain: 'aoneahsan-portfolio.firebaseapp.com',
|
|
43
|
+
projectId: 'aoneahsan-portfolio',
|
|
44
|
+
},
|
|
45
|
+
projectId: 'your-project-id',
|
|
46
|
+
projectName: 'Your Project Name',
|
|
47
|
+
platform: 'web', // or 'android', 'ios', 'extension'
|
|
48
|
+
debug: import.meta.env.DEV,
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 2. Add environment variables
|
|
53
|
+
|
|
54
|
+
Create or update your `.env` file:
|
|
55
|
+
|
|
56
|
+
```env
|
|
57
|
+
# shared-features configuration
|
|
58
|
+
VITE_ZAIONS_FIREBASE_API_KEY=your-api-key
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 3. Initialize on app startup
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// src/main.tsx or src/App.tsx
|
|
65
|
+
import { initSharedFeatures } from 'shared-features';
|
|
66
|
+
import './config/shared-features'; // Import to initialize
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
### Displaying Ads
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { AdPanel, useCampaigns } from 'shared-features';
|
|
75
|
+
|
|
76
|
+
// Simple panel in sidebar
|
|
77
|
+
function Sidebar() {
|
|
78
|
+
return <AdPanel placement="sidebar_panel" />;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Custom implementation with hook
|
|
82
|
+
function Footer() {
|
|
83
|
+
const { campaigns, loading, recordImpression, recordClick } = useCampaigns({
|
|
84
|
+
placement: 'footer_slider',
|
|
85
|
+
maxCampaigns: 5,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (loading || campaigns.length === 0) return null;
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div>
|
|
92
|
+
{campaigns.map(campaign => (
|
|
93
|
+
<div
|
|
94
|
+
key={campaign.id}
|
|
95
|
+
onMouseEnter={() => recordImpression(campaign)}
|
|
96
|
+
onClick={() => {
|
|
97
|
+
recordClick(campaign);
|
|
98
|
+
window.open(campaign.product.url, '_blank');
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
<h3>{campaign.product.name}</h3>
|
|
102
|
+
<p>{campaign.product.tagline}</p>
|
|
103
|
+
</div>
|
|
104
|
+
))}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Available Placements
|
|
111
|
+
|
|
112
|
+
| Placement | Description | Recommended Variant |
|
|
113
|
+
|-----------|-------------|-------------------|
|
|
114
|
+
| `popup_slider` | Extension popup | Small variants |
|
|
115
|
+
| `options_panel` | Extension options page | Small variants |
|
|
116
|
+
| `onetime_modal` | First-visit welcome modal | Large variants |
|
|
117
|
+
| `update_modal` | Version update modal | Large variants |
|
|
118
|
+
| `notification` | Push notification | - |
|
|
119
|
+
| `footer_slider` | Web app footer | Small variants |
|
|
120
|
+
| `sidebar_panel` | Web app sidebar | Small variants |
|
|
121
|
+
| `home_banner` | Home page hero | Large variants |
|
|
122
|
+
|
|
123
|
+
### Available Variants
|
|
124
|
+
|
|
125
|
+
**Small Variants (compact spaces):**
|
|
126
|
+
- `small_panel_1` - Minimal
|
|
127
|
+
- `small_panel_2` - Tagline
|
|
128
|
+
- `small_panel_3` - Features
|
|
129
|
+
- `small_panel_4` - Gradient
|
|
130
|
+
- `small_panel_5` - Card
|
|
131
|
+
|
|
132
|
+
**Large Variants (feature areas):**
|
|
133
|
+
- `large_slider_1` - Hero
|
|
134
|
+
- `large_slider_2` - Feature Grid
|
|
135
|
+
- `large_slider_3` - Testimonial
|
|
136
|
+
- `large_slider_4` - Comparison
|
|
137
|
+
- `large_slider_5` - Video Placeholder
|
|
138
|
+
|
|
139
|
+
## API Reference
|
|
140
|
+
|
|
141
|
+
### `initSharedFeatures(config)`
|
|
142
|
+
|
|
143
|
+
Initialize the package with your configuration.
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
interface SharedFeaturesConfig {
|
|
147
|
+
firebaseConfig: FirebaseConfig;
|
|
148
|
+
projectId: string;
|
|
149
|
+
projectName: string;
|
|
150
|
+
platform: 'web' | 'android' | 'ios' | 'extension';
|
|
151
|
+
debug?: boolean;
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### `useCampaigns(options)`
|
|
156
|
+
|
|
157
|
+
Hook to fetch and manage campaigns.
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
interface UseCampaignsOptions {
|
|
161
|
+
placement: AdPlacement;
|
|
162
|
+
maxCampaigns?: number;
|
|
163
|
+
autoFetch?: boolean;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
interface UseCampaignsResult {
|
|
167
|
+
campaigns: CampaignWithProduct[];
|
|
168
|
+
campaign: CampaignWithProduct | null;
|
|
169
|
+
loading: boolean;
|
|
170
|
+
error: string | null;
|
|
171
|
+
refetch: () => Promise<void>;
|
|
172
|
+
recordImpression: (campaign: CampaignWithProduct) => Promise<void>;
|
|
173
|
+
recordClick: (campaign: CampaignWithProduct) => Promise<void>;
|
|
174
|
+
recordClose: (campaign: CampaignWithProduct) => Promise<void>;
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### `AdPanel`
|
|
179
|
+
|
|
180
|
+
Component to display a single ad.
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
<AdPanel
|
|
184
|
+
placement="sidebar_panel"
|
|
185
|
+
variant="small_panel_2"
|
|
186
|
+
className="my-ad"
|
|
187
|
+
/>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Frequency Capping
|
|
191
|
+
|
|
192
|
+
Ads automatically respect frequency capping set in the admin panel. By default, each campaign is shown to a user once every 20 days. This is tracked locally using Capacitor Preferences (mobile) or localStorage (web).
|
|
193
|
+
|
|
194
|
+
## Admin Panel
|
|
195
|
+
|
|
196
|
+
All campaigns are managed through the admin panel at [aoneahsan.com/admin/campaigns](https://aoneahsan.com/admin/campaigns). Consumer projects only need read access.
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT - Ahsan Mahmood <aoneahsan@gmail.com>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
3
|
+
const react = require("react");
|
|
4
|
+
const themes = require("@radix-ui/themes");
|
|
5
|
+
const reactIcons = require("@radix-ui/react-icons");
|
|
6
|
+
const useCampaigns = require("./useCampaigns-BNOHpETm.cjs");
|
|
7
|
+
function AdPanel({
|
|
8
|
+
placement,
|
|
9
|
+
variant: _variant = "small_panel_2",
|
|
10
|
+
className
|
|
11
|
+
}) {
|
|
12
|
+
const { campaign, loading, error, recordImpression, recordClick, recordClose } = useCampaigns.useCampaign({ placement });
|
|
13
|
+
const hasRecordedImpression = react.useRef(false);
|
|
14
|
+
react.useEffect(() => {
|
|
15
|
+
if (campaign && !hasRecordedImpression.current) {
|
|
16
|
+
hasRecordedImpression.current = true;
|
|
17
|
+
recordImpression(campaign);
|
|
18
|
+
}
|
|
19
|
+
}, [campaign, recordImpression]);
|
|
20
|
+
if (loading || error || !campaign) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const handleClick = () => {
|
|
24
|
+
recordClick(campaign);
|
|
25
|
+
const url = campaign.customCtaUrl || campaign.product.url;
|
|
26
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
27
|
+
};
|
|
28
|
+
const handleClose = () => {
|
|
29
|
+
recordClose(campaign);
|
|
30
|
+
};
|
|
31
|
+
const title = campaign.customTitle || campaign.product.name;
|
|
32
|
+
const tagline = campaign.customTagline || campaign.product.tagline;
|
|
33
|
+
const ctaText = campaign.customCta || "Learn More";
|
|
34
|
+
const color = campaign.customProductColor || campaign.product.color;
|
|
35
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
36
|
+
themes.Box,
|
|
37
|
+
{
|
|
38
|
+
className,
|
|
39
|
+
style: {
|
|
40
|
+
border: `1px solid ${color}`,
|
|
41
|
+
borderRadius: "8px",
|
|
42
|
+
padding: "12px",
|
|
43
|
+
backgroundColor: `${color}10`,
|
|
44
|
+
position: "relative"
|
|
45
|
+
},
|
|
46
|
+
children: [
|
|
47
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
48
|
+
themes.IconButton,
|
|
49
|
+
{
|
|
50
|
+
size: "1",
|
|
51
|
+
variant: "ghost",
|
|
52
|
+
onClick: handleClose,
|
|
53
|
+
style: {
|
|
54
|
+
position: "absolute",
|
|
55
|
+
top: "4px",
|
|
56
|
+
right: "4px"
|
|
57
|
+
},
|
|
58
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.Cross2Icon, {})
|
|
59
|
+
}
|
|
60
|
+
),
|
|
61
|
+
/* @__PURE__ */ jsxRuntime.jsxs(themes.Flex, { direction: "column", gap: "2", children: [
|
|
62
|
+
/* @__PURE__ */ jsxRuntime.jsxs(themes.Flex, { align: "center", gap: "2", children: [
|
|
63
|
+
campaign.product.icon64 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
64
|
+
themes.Box,
|
|
65
|
+
{
|
|
66
|
+
dangerouslySetInnerHTML: { __html: campaign.product.icon64 },
|
|
67
|
+
style: { width: 32, height: 32 }
|
|
68
|
+
}
|
|
69
|
+
),
|
|
70
|
+
/* @__PURE__ */ jsxRuntime.jsx(themes.Text, { weight: "bold", size: "3", style: { color }, children: title })
|
|
71
|
+
] }),
|
|
72
|
+
/* @__PURE__ */ jsxRuntime.jsx(themes.Text, { size: "2", color: "gray", children: tagline }),
|
|
73
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
74
|
+
themes.Button,
|
|
75
|
+
{
|
|
76
|
+
size: "2",
|
|
77
|
+
onClick: handleClick,
|
|
78
|
+
style: { backgroundColor: color },
|
|
79
|
+
children: ctaText
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
] })
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
exports.AdPanel = AdPanel;
|
|
88
|
+
//# sourceMappingURL=AdPanel-D0BiV6Xb.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AdPanel-D0BiV6Xb.cjs","sources":["../src/components/ads/AdPanel.tsx"],"sourcesContent":["/**\n * AdPanel Component\n *\n * A simple ad panel component that displays a single campaign.\n * Can be placed in sidebars, footers, or other static locations.\n *\n * @author Ahsan Mahmood <aoneahsan@gmail.com>\n */\n\nimport { useEffect, useRef } from 'react';\nimport { Box, Flex, Text, Button, IconButton } from '@radix-ui/themes';\nimport { Cross2Icon } from '@radix-ui/react-icons';\nimport { useCampaign } from '../../hooks/useCampaigns';\nimport type { AdPanelProps } from '../../types/campaigns';\n\n/**\n * AdPanel displays a single campaign in a compact panel format.\n *\n * @example\n * ```tsx\n * <AdPanel placement=\"sidebar_panel\" variant=\"small_panel_2\" />\n * ```\n */\nexport function AdPanel({\n placement,\n variant: _variant = 'small_panel_2',\n className,\n}: AdPanelProps) {\n // TODO: variant will be used for different display styles in future variants\n const { campaign, loading, error, recordImpression, recordClick, recordClose } =\n useCampaign({ placement });\n\n const hasRecordedImpression = useRef(false);\n\n // Record impression when campaign is first displayed\n useEffect(() => {\n if (campaign && !hasRecordedImpression.current) {\n hasRecordedImpression.current = true;\n recordImpression(campaign);\n }\n }, [campaign, recordImpression]);\n\n if (loading || error || !campaign) {\n return null;\n }\n\n const handleClick = () => {\n recordClick(campaign);\n const url = campaign.customCtaUrl || campaign.product.url;\n window.open(url, '_blank', 'noopener,noreferrer');\n };\n\n const handleClose = () => {\n recordClose(campaign);\n // Hide the panel (could use state or CSS)\n };\n\n const title = campaign.customTitle || campaign.product.name;\n const tagline = campaign.customTagline || campaign.product.tagline;\n const ctaText = campaign.customCta || 'Learn More';\n const color = campaign.customProductColor || campaign.product.color;\n\n return (\n <Box\n className={className}\n style={{\n border: `1px solid ${color}`,\n borderRadius: '8px',\n padding: '12px',\n backgroundColor: `${color}10`,\n position: 'relative',\n }}\n >\n <IconButton\n size=\"1\"\n variant=\"ghost\"\n onClick={handleClose}\n style={{\n position: 'absolute',\n top: '4px',\n right: '4px',\n }}\n >\n <Cross2Icon />\n </IconButton>\n\n <Flex direction=\"column\" gap=\"2\">\n <Flex align=\"center\" gap=\"2\">\n {campaign.product.icon64 && (\n <Box\n dangerouslySetInnerHTML={{ __html: campaign.product.icon64 }}\n style={{ width: 32, height: 32 }}\n />\n )}\n <Text weight=\"bold\" size=\"3\" style={{ color }}>\n {title}\n </Text>\n </Flex>\n\n <Text size=\"2\" color=\"gray\">\n {tagline}\n </Text>\n\n <Button\n size=\"2\"\n onClick={handleClick}\n style={{ backgroundColor: color }}\n >\n {ctaText}\n </Button>\n </Flex>\n </Box>\n );\n}\n\nexport default AdPanel;\n"],"names":["useCampaign","useRef","useEffect","jsxs","Box","jsx","IconButton","Cross2Icon","Flex","Text","Button"],"mappings":";;;;;;AAuBO,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA,SAAS,WAAW;AAAA,EACpB;AACF,GAAiB;AAEf,QAAM,EAAE,UAAU,SAAS,OAAO,kBAAkB,aAAa,gBAC/DA,aAAAA,YAAY,EAAE,WAAW;AAE3B,QAAM,wBAAwBC,MAAAA,OAAO,KAAK;AAG1CC,QAAAA,UAAU,MAAM;AACd,QAAI,YAAY,CAAC,sBAAsB,SAAS;AAC9C,4BAAsB,UAAU;AAChC,uBAAiB,QAAQ;AAAA,IAC3B;AAAA,EACF,GAAG,CAAC,UAAU,gBAAgB,CAAC;AAE/B,MAAI,WAAW,SAAS,CAAC,UAAU;AACjC,WAAO;AAAA,EACT;AAEA,QAAM,cAAc,MAAM;AACxB,gBAAY,QAAQ;AACpB,UAAM,MAAM,SAAS,gBAAgB,SAAS,QAAQ;AACtD,WAAO,KAAK,KAAK,UAAU,qBAAqB;AAAA,EAClD;AAEA,QAAM,cAAc,MAAM;AACxB,gBAAY,QAAQ;AAAA,EAEtB;AAEA,QAAM,QAAQ,SAAS,eAAe,SAAS,QAAQ;AACvD,QAAM,UAAU,SAAS,iBAAiB,SAAS,QAAQ;AAC3D,QAAM,UAAU,SAAS,aAAa;AACtC,QAAM,QAAQ,SAAS,sBAAsB,SAAS,QAAQ;AAE9D,SACEC,2BAAAA;AAAAA,IAACC,OAAAA;AAAAA,IAAA;AAAA,MACC;AAAA,MACA,OAAO;AAAA,QACL,QAAQ,aAAa,KAAK;AAAA,QAC1B,cAAc;AAAA,QACd,SAAS;AAAA,QACT,iBAAiB,GAAG,KAAK;AAAA,QACzB,UAAU;AAAA,MAAA;AAAA,MAGZ,UAAA;AAAA,QAAAC,2BAAAA;AAAAA,UAACC,OAAAA;AAAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,SAAS;AAAA,YACT,OAAO;AAAA,cACL,UAAU;AAAA,cACV,KAAK;AAAA,cACL,OAAO;AAAA,YAAA;AAAA,YAGT,yCAACC,WAAAA,YAAA,CAAA,CAAW;AAAA,UAAA;AAAA,QAAA;AAAA,QAGdJ,2BAAAA,KAACK,OAAAA,MAAA,EAAK,WAAU,UAAS,KAAI,KAC3B,UAAA;AAAA,UAAAL,2BAAAA,KAACK,OAAAA,MAAA,EAAK,OAAM,UAAS,KAAI,KACtB,UAAA;AAAA,YAAA,SAAS,QAAQ,UAChBH,2BAAAA;AAAAA,cAACD,OAAAA;AAAAA,cAAA;AAAA,gBACC,yBAAyB,EAAE,QAAQ,SAAS,QAAQ,OAAA;AAAA,gBACpD,OAAO,EAAE,OAAO,IAAI,QAAQ,GAAA;AAAA,cAAG;AAAA,YAAA;AAAA,YAGnCC,2BAAAA,IAACI,OAAAA,MAAA,EAAK,QAAO,QAAO,MAAK,KAAI,OAAO,EAAE,SACnC,UAAA,MAAA,CACH;AAAA,UAAA,GACF;AAAA,yCAECA,OAAAA,MAAA,EAAK,MAAK,KAAI,OAAM,QAClB,UAAA,SACH;AAAA,UAEAJ,2BAAAA;AAAAA,YAACK,OAAAA;AAAAA,YAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS;AAAA,cACT,OAAO,EAAE,iBAAiB,MAAA;AAAA,cAEzB,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,QACH,EAAA,CACF;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN;;"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useEffect } from "react";
|
|
3
|
+
import { Box, IconButton, Flex, Text, Button } from "@radix-ui/themes";
|
|
4
|
+
import { Cross2Icon } from "@radix-ui/react-icons";
|
|
5
|
+
import { a as useCampaign } from "./useCampaigns-3NxODLLs.js";
|
|
6
|
+
function AdPanel({
|
|
7
|
+
placement,
|
|
8
|
+
variant: _variant = "small_panel_2",
|
|
9
|
+
className
|
|
10
|
+
}) {
|
|
11
|
+
const { campaign, loading, error, recordImpression, recordClick, recordClose } = useCampaign({ placement });
|
|
12
|
+
const hasRecordedImpression = useRef(false);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (campaign && !hasRecordedImpression.current) {
|
|
15
|
+
hasRecordedImpression.current = true;
|
|
16
|
+
recordImpression(campaign);
|
|
17
|
+
}
|
|
18
|
+
}, [campaign, recordImpression]);
|
|
19
|
+
if (loading || error || !campaign) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const handleClick = () => {
|
|
23
|
+
recordClick(campaign);
|
|
24
|
+
const url = campaign.customCtaUrl || campaign.product.url;
|
|
25
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
26
|
+
};
|
|
27
|
+
const handleClose = () => {
|
|
28
|
+
recordClose(campaign);
|
|
29
|
+
};
|
|
30
|
+
const title = campaign.customTitle || campaign.product.name;
|
|
31
|
+
const tagline = campaign.customTagline || campaign.product.tagline;
|
|
32
|
+
const ctaText = campaign.customCta || "Learn More";
|
|
33
|
+
const color = campaign.customProductColor || campaign.product.color;
|
|
34
|
+
return /* @__PURE__ */ jsxs(
|
|
35
|
+
Box,
|
|
36
|
+
{
|
|
37
|
+
className,
|
|
38
|
+
style: {
|
|
39
|
+
border: `1px solid ${color}`,
|
|
40
|
+
borderRadius: "8px",
|
|
41
|
+
padding: "12px",
|
|
42
|
+
backgroundColor: `${color}10`,
|
|
43
|
+
position: "relative"
|
|
44
|
+
},
|
|
45
|
+
children: [
|
|
46
|
+
/* @__PURE__ */ jsx(
|
|
47
|
+
IconButton,
|
|
48
|
+
{
|
|
49
|
+
size: "1",
|
|
50
|
+
variant: "ghost",
|
|
51
|
+
onClick: handleClose,
|
|
52
|
+
style: {
|
|
53
|
+
position: "absolute",
|
|
54
|
+
top: "4px",
|
|
55
|
+
right: "4px"
|
|
56
|
+
},
|
|
57
|
+
children: /* @__PURE__ */ jsx(Cross2Icon, {})
|
|
58
|
+
}
|
|
59
|
+
),
|
|
60
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", gap: "2", children: [
|
|
61
|
+
/* @__PURE__ */ jsxs(Flex, { align: "center", gap: "2", children: [
|
|
62
|
+
campaign.product.icon64 && /* @__PURE__ */ jsx(
|
|
63
|
+
Box,
|
|
64
|
+
{
|
|
65
|
+
dangerouslySetInnerHTML: { __html: campaign.product.icon64 },
|
|
66
|
+
style: { width: 32, height: 32 }
|
|
67
|
+
}
|
|
68
|
+
),
|
|
69
|
+
/* @__PURE__ */ jsx(Text, { weight: "bold", size: "3", style: { color }, children: title })
|
|
70
|
+
] }),
|
|
71
|
+
/* @__PURE__ */ jsx(Text, { size: "2", color: "gray", children: tagline }),
|
|
72
|
+
/* @__PURE__ */ jsx(
|
|
73
|
+
Button,
|
|
74
|
+
{
|
|
75
|
+
size: "2",
|
|
76
|
+
onClick: handleClick,
|
|
77
|
+
style: { backgroundColor: color },
|
|
78
|
+
children: ctaText
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
] })
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
export {
|
|
87
|
+
AdPanel as A
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=AdPanel-RGRBf4ub.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AdPanel-RGRBf4ub.js","sources":["../src/components/ads/AdPanel.tsx"],"sourcesContent":["/**\n * AdPanel Component\n *\n * A simple ad panel component that displays a single campaign.\n * Can be placed in sidebars, footers, or other static locations.\n *\n * @author Ahsan Mahmood <aoneahsan@gmail.com>\n */\n\nimport { useEffect, useRef } from 'react';\nimport { Box, Flex, Text, Button, IconButton } from '@radix-ui/themes';\nimport { Cross2Icon } from '@radix-ui/react-icons';\nimport { useCampaign } from '../../hooks/useCampaigns';\nimport type { AdPanelProps } from '../../types/campaigns';\n\n/**\n * AdPanel displays a single campaign in a compact panel format.\n *\n * @example\n * ```tsx\n * <AdPanel placement=\"sidebar_panel\" variant=\"small_panel_2\" />\n * ```\n */\nexport function AdPanel({\n placement,\n variant: _variant = 'small_panel_2',\n className,\n}: AdPanelProps) {\n // TODO: variant will be used for different display styles in future variants\n const { campaign, loading, error, recordImpression, recordClick, recordClose } =\n useCampaign({ placement });\n\n const hasRecordedImpression = useRef(false);\n\n // Record impression when campaign is first displayed\n useEffect(() => {\n if (campaign && !hasRecordedImpression.current) {\n hasRecordedImpression.current = true;\n recordImpression(campaign);\n }\n }, [campaign, recordImpression]);\n\n if (loading || error || !campaign) {\n return null;\n }\n\n const handleClick = () => {\n recordClick(campaign);\n const url = campaign.customCtaUrl || campaign.product.url;\n window.open(url, '_blank', 'noopener,noreferrer');\n };\n\n const handleClose = () => {\n recordClose(campaign);\n // Hide the panel (could use state or CSS)\n };\n\n const title = campaign.customTitle || campaign.product.name;\n const tagline = campaign.customTagline || campaign.product.tagline;\n const ctaText = campaign.customCta || 'Learn More';\n const color = campaign.customProductColor || campaign.product.color;\n\n return (\n <Box\n className={className}\n style={{\n border: `1px solid ${color}`,\n borderRadius: '8px',\n padding: '12px',\n backgroundColor: `${color}10`,\n position: 'relative',\n }}\n >\n <IconButton\n size=\"1\"\n variant=\"ghost\"\n onClick={handleClose}\n style={{\n position: 'absolute',\n top: '4px',\n right: '4px',\n }}\n >\n <Cross2Icon />\n </IconButton>\n\n <Flex direction=\"column\" gap=\"2\">\n <Flex align=\"center\" gap=\"2\">\n {campaign.product.icon64 && (\n <Box\n dangerouslySetInnerHTML={{ __html: campaign.product.icon64 }}\n style={{ width: 32, height: 32 }}\n />\n )}\n <Text weight=\"bold\" size=\"3\" style={{ color }}>\n {title}\n </Text>\n </Flex>\n\n <Text size=\"2\" color=\"gray\">\n {tagline}\n </Text>\n\n <Button\n size=\"2\"\n onClick={handleClick}\n style={{ backgroundColor: color }}\n >\n {ctaText}\n </Button>\n </Flex>\n </Box>\n );\n}\n\nexport default AdPanel;\n"],"names":[],"mappings":";;;;;AAuBO,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA,SAAS,WAAW;AAAA,EACpB;AACF,GAAiB;AAEf,QAAM,EAAE,UAAU,SAAS,OAAO,kBAAkB,aAAa,gBAC/D,YAAY,EAAE,WAAW;AAE3B,QAAM,wBAAwB,OAAO,KAAK;AAG1C,YAAU,MAAM;AACd,QAAI,YAAY,CAAC,sBAAsB,SAAS;AAC9C,4BAAsB,UAAU;AAChC,uBAAiB,QAAQ;AAAA,IAC3B;AAAA,EACF,GAAG,CAAC,UAAU,gBAAgB,CAAC;AAE/B,MAAI,WAAW,SAAS,CAAC,UAAU;AACjC,WAAO;AAAA,EACT;AAEA,QAAM,cAAc,MAAM;AACxB,gBAAY,QAAQ;AACpB,UAAM,MAAM,SAAS,gBAAgB,SAAS,QAAQ;AACtD,WAAO,KAAK,KAAK,UAAU,qBAAqB;AAAA,EAClD;AAEA,QAAM,cAAc,MAAM;AACxB,gBAAY,QAAQ;AAAA,EAEtB;AAEA,QAAM,QAAQ,SAAS,eAAe,SAAS,QAAQ;AACvD,QAAM,UAAU,SAAS,iBAAiB,SAAS,QAAQ;AAC3D,QAAM,UAAU,SAAS,aAAa;AACtC,QAAM,QAAQ,SAAS,sBAAsB,SAAS,QAAQ;AAE9D,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,OAAO;AAAA,QACL,QAAQ,aAAa,KAAK;AAAA,QAC1B,cAAc;AAAA,QACd,SAAS;AAAA,QACT,iBAAiB,GAAG,KAAK;AAAA,QACzB,UAAU;AAAA,MAAA;AAAA,MAGZ,UAAA;AAAA,QAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,SAAS;AAAA,YACT,OAAO;AAAA,cACL,UAAU;AAAA,cACV,KAAK;AAAA,cACL,OAAO;AAAA,YAAA;AAAA,YAGT,8BAAC,YAAA,CAAA,CAAW;AAAA,UAAA;AAAA,QAAA;AAAA,QAGd,qBAAC,MAAA,EAAK,WAAU,UAAS,KAAI,KAC3B,UAAA;AAAA,UAAA,qBAAC,MAAA,EAAK,OAAM,UAAS,KAAI,KACtB,UAAA;AAAA,YAAA,SAAS,QAAQ,UAChB;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,yBAAyB,EAAE,QAAQ,SAAS,QAAQ,OAAA;AAAA,gBACpD,OAAO,EAAE,OAAO,IAAI,QAAQ,GAAA;AAAA,cAAG;AAAA,YAAA;AAAA,YAGnC,oBAAC,MAAA,EAAK,QAAO,QAAO,MAAK,KAAI,OAAO,EAAE,SACnC,UAAA,MAAA,CACH;AAAA,UAAA,GACF;AAAA,8BAEC,MAAA,EAAK,MAAK,KAAI,OAAM,QAClB,UAAA,SACH;AAAA,UAEA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS;AAAA,cACT,OAAO,EAAE,iBAAiB,MAAA;AAAA,cAEzB,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,QACH,EAAA,CACF;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN;"}
|