@adland/react 0.12.1 → 0.13.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/CHANGELOG.md +22 -1
- package/package.json +4 -2
- package/src/components/Ad.tsx +146 -164
- package/src/fetch.ts +63 -12
- package/src/hooks/useAdContext.ts +18 -0
- package/src/index.ts +33 -11
- package/src/types.ts +15 -16
- package/src/utils/ad-actions.ts +26 -0
- package/src/utils/ad-fields.ts +45 -0
- package/LICENSE +0 -21
- package/src/components/BasicAdBody.tsx +0 -27
- package/src/components/content/CastAdContent.tsx +0 -56
- package/src/components/content/FarcasterProfileAdContent.tsx +0 -79
- package/src/components/content/LinkAdContent.tsx +0 -49
- package/src/components/content/MiniappAdContent.tsx +0 -48
- package/src/components/content/TokenAdContent.tsx +0 -56
- package/src/utils/index.ts +0 -22
- package/src/utils/relativeTime.ts +0 -44
- package/src/utils/sdk.ts +0 -159
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @adland/react
|
|
2
2
|
|
|
3
|
+
## 0.13.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 71a90f5: Export AdContent component for rendering ad data without on-chain fetching
|
|
8
|
+
- 8877133: refactor
|
|
9
|
+
- Updated dependencies [8877133]
|
|
10
|
+
- @adland/data@0.14.1
|
|
11
|
+
|
|
12
|
+
## 0.13.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- c051bc3: repo migration
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- 0d3484f: centralized packages
|
|
21
|
+
- Updated dependencies [c051bc3]
|
|
22
|
+
- Updated dependencies [0d3484f]
|
|
23
|
+
- @adland/data@0.14.0
|
|
24
|
+
|
|
3
25
|
## 0.12.1
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
|
@@ -121,7 +143,6 @@
|
|
|
121
143
|
### Minor Changes
|
|
122
144
|
|
|
123
145
|
- c2df20a: Add `useFetch` hook with global in-memory cache and improve Ad component:
|
|
124
|
-
|
|
125
146
|
- Add `useFetch` hook with React Query-like global cache that persists across component lifecycles
|
|
126
147
|
- Add `fetchCache` utility for global cache management
|
|
127
148
|
- Improve Ad component to use relative URLs for local development
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adland/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
"@types/react-dom": "^18.3.1",
|
|
34
34
|
"lucide-react": "0.561.0",
|
|
35
35
|
"tsup": "^8.0.1",
|
|
36
|
-
"
|
|
36
|
+
"viem": "^2.0.0",
|
|
37
|
+
"@adland/data": "0.14.1",
|
|
38
|
+
"@0xslots/sdk": "0.10.1"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
39
41
|
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
package/src/components/Ad.tsx
CHANGED
|
@@ -1,206 +1,188 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
import {
|
|
1
|
+
import { SlotsChain } from "@0xslots/sdk";
|
|
2
|
+
import type { AdData } from "@adland/data";
|
|
3
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
4
|
+
|
|
5
|
+
import { createReadClient, fetchAdFromURI, fetchMetadataURI } from "../fetch";
|
|
6
|
+
import { AdContext, useAd } from "../hooks/useAdContext";
|
|
6
7
|
import { useFetch } from "../hooks/useFetch";
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import { AdDataQueryError, AdProps } from "../types";
|
|
14
|
-
import { getBaseUrl } from "../utils";
|
|
15
|
-
import { adlandApiUrl } from "../utils/constants";
|
|
16
|
-
import { fetchAd } from "../fetch";
|
|
8
|
+
import { AdDataQueryError, type AdProps } from "../types";
|
|
9
|
+
import { performAdAction } from "../utils/ad-actions";
|
|
10
|
+
import { getAdDescription, getAdImage, getAdTitle, getAdType } from "../utils/ad-fields";
|
|
11
|
+
import { adCardIcon, adCardLabel } from "../utils/constants";
|
|
12
|
+
|
|
13
|
+
// ─── Root component ──────────────────────────────────────────────────────────
|
|
17
14
|
|
|
18
15
|
/**
|
|
19
|
-
*
|
|
20
|
-
* Uses Farcaster MiniApp SDK for authenticated tracking requests
|
|
16
|
+
* Root Ad component — compound pattern.
|
|
21
17
|
*
|
|
22
18
|
* @example
|
|
23
19
|
* ```tsx
|
|
24
|
-
* <Ad
|
|
25
|
-
* <
|
|
20
|
+
* <Ad slot="0xabc...123" className="rounded-md border p-3">
|
|
21
|
+
* <AdImage className="size-10 rounded-md" />
|
|
22
|
+
* <AdTitle className="text-sm font-medium" />
|
|
23
|
+
* <AdDescription className="text-xs text-muted-foreground" />
|
|
24
|
+
* <AdBadge />
|
|
26
25
|
* </Ad>
|
|
27
26
|
* ```
|
|
28
27
|
*/
|
|
29
28
|
export function Ad({
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
slot,
|
|
30
|
+
data: staticData,
|
|
31
|
+
chainId = SlotsChain.BASE,
|
|
32
|
+
rpcUrl,
|
|
33
|
+
children,
|
|
34
34
|
...props
|
|
35
35
|
}: AdProps) {
|
|
36
36
|
const ref = useRef<HTMLDivElement>(null);
|
|
37
|
+
|
|
38
|
+
const client = useMemo(
|
|
39
|
+
() => (slot ? createReadClient(chainId, rpcUrl) : null),
|
|
40
|
+
[slot, chainId, rpcUrl],
|
|
41
|
+
);
|
|
42
|
+
|
|
37
43
|
const {
|
|
38
|
-
data:
|
|
44
|
+
data: fetchedData,
|
|
39
45
|
isLoading,
|
|
40
46
|
error,
|
|
41
47
|
} = useFetch<AdData>(
|
|
42
|
-
`ad-data-${
|
|
43
|
-
() =>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const networkBaseUrl = baseUrl ?? getBaseUrl(network);
|
|
49
|
-
|
|
50
|
-
const send = useCallback(
|
|
51
|
-
(type: "view" | "click") => {
|
|
52
|
-
const trackEndpoint = `${networkBaseUrl}/api/analytics/track`;
|
|
53
|
-
|
|
54
|
-
sendTrackRequest(trackEndpoint, {
|
|
55
|
-
type,
|
|
56
|
-
adId: slotId,
|
|
57
|
-
adOwner: owner,
|
|
58
|
-
}).catch((error: unknown) => {
|
|
59
|
-
console.error(`[@adland/react] Failed to track ${type}:`, error);
|
|
60
|
-
});
|
|
48
|
+
`ad-data-${slot}`,
|
|
49
|
+
async () => {
|
|
50
|
+
if (!client || !slot) throw new Error(AdDataQueryError.NO_AD);
|
|
51
|
+
const uri = await fetchMetadataURI(client, slot);
|
|
52
|
+
if (!uri) throw new Error(AdDataQueryError.NO_AD);
|
|
53
|
+
return fetchAdFromURI(uri);
|
|
61
54
|
},
|
|
62
|
-
|
|
55
|
+
{ enabled: !!slot && !staticData },
|
|
63
56
|
);
|
|
64
57
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!entry?.isIntersecting) return;
|
|
75
|
-
|
|
76
|
-
const already = sessionStorage.getItem(key);
|
|
77
|
-
if (already) {
|
|
78
|
-
obs.unobserve(el);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
sessionStorage.setItem(key, "1");
|
|
83
|
-
send("view");
|
|
84
|
-
obs.unobserve(el);
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
threshold: 0.15, // more forgiving
|
|
88
|
-
},
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
obs.observe(el);
|
|
92
|
-
return () => obs.disconnect();
|
|
93
|
-
}, [slotId, send]);
|
|
94
|
-
|
|
95
|
-
// CLICK tracking
|
|
96
|
-
// Only track clicks that aren't on links or buttons (let those handle their own navigation)
|
|
58
|
+
const adData = staticData ?? fetchedData;
|
|
59
|
+
|
|
60
|
+
const isEmpty =
|
|
61
|
+
!adData &&
|
|
62
|
+
!isLoading &&
|
|
63
|
+
(error instanceof Error
|
|
64
|
+
? error.message === AdDataQueryError.NO_AD
|
|
65
|
+
: !error);
|
|
66
|
+
|
|
97
67
|
const onClick = useCallback(
|
|
98
68
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
99
|
-
// Don't track if clicking on a link, button, or other interactive element
|
|
100
69
|
const target = e.target as HTMLElement;
|
|
101
|
-
const
|
|
70
|
+
const isInteractive =
|
|
102
71
|
target.tagName === "A" ||
|
|
103
72
|
target.tagName === "BUTTON" ||
|
|
104
73
|
target.closest("a") !== null ||
|
|
105
74
|
target.closest("button") !== null;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
send("click");
|
|
75
|
+
if (!isInteractive && adData) {
|
|
76
|
+
performAdAction(adData);
|
|
109
77
|
}
|
|
110
78
|
},
|
|
111
|
-
[
|
|
79
|
+
[adData],
|
|
112
80
|
);
|
|
113
81
|
|
|
114
82
|
return (
|
|
115
|
-
<
|
|
116
|
-
{
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
if (error.message === AdDataQueryError.ERROR) {
|
|
131
|
-
return <ErrorAdContent error={error} />;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return <ErrorAdContent error={error} />;
|
|
135
|
-
}
|
|
136
|
-
if (adData) {
|
|
137
|
-
return (
|
|
138
|
-
<>
|
|
139
|
-
{adData.type === "link" && <LinkAdContent data={adData} />}
|
|
140
|
-
|
|
141
|
-
{adData.type === "cast" && <CastAdContent data={adData} />}
|
|
142
|
-
|
|
143
|
-
{adData.type === "miniapp" && <MiniappAdContent data={adData} />}
|
|
144
|
-
|
|
145
|
-
{adData.type === "token" && <TokenAdContent data={adData} />}
|
|
146
|
-
|
|
147
|
-
{adData.type === "farcasterProfile" && (
|
|
148
|
-
<FarcasterProfileAdContent data={adData} />
|
|
149
|
-
)}
|
|
150
|
-
</>
|
|
151
|
-
);
|
|
152
|
-
} else {
|
|
153
|
-
return (
|
|
154
|
-
<EmtpyAdContent
|
|
155
|
-
data={{ url: networkBaseUrl + `${owner}/${slotId}` }}
|
|
156
|
-
/>
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
})()}
|
|
160
|
-
</div>
|
|
83
|
+
<AdContext.Provider
|
|
84
|
+
value={{
|
|
85
|
+
data: adData ?? null,
|
|
86
|
+
isLoading: !!slot && !staticData && isLoading,
|
|
87
|
+
error,
|
|
88
|
+
isEmpty,
|
|
89
|
+
slot,
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<div ref={ref} onClick={onClick} {...props}>
|
|
93
|
+
{children}
|
|
94
|
+
</div>
|
|
95
|
+
</AdContext.Provider>
|
|
161
96
|
);
|
|
162
97
|
}
|
|
163
98
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
<FileWarningIcon className="w-5 h-5 md:w-7 md:h-7" />
|
|
169
|
-
<p className="font-bold text-primary">ERROR</p>
|
|
170
|
-
</div>
|
|
171
|
-
</BasicAdBody>
|
|
172
|
-
);
|
|
99
|
+
// ─── Sub-components ──────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
export interface AdImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
|
102
|
+
fallback?: React.ReactNode;
|
|
173
103
|
}
|
|
174
104
|
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
105
|
+
export function AdImage({ fallback, ...props }: AdImageProps) {
|
|
106
|
+
const { data } = useAd();
|
|
107
|
+
const src = getAdImage(data);
|
|
108
|
+
if (!src) return fallback ? <>{fallback}</> : null;
|
|
109
|
+
return <img src={src} alt="" {...props} />;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface AdTitleProps extends React.HTMLAttributes<HTMLParagraphElement> {
|
|
113
|
+
fallback?: React.ReactNode;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function AdTitle({ fallback, children, ...props }: AdTitleProps) {
|
|
117
|
+
const { data } = useAd();
|
|
118
|
+
const title = getAdTitle(data);
|
|
119
|
+
if (!title) return fallback ? <>{fallback}</> : null;
|
|
120
|
+
return <p {...props}>{children ?? title}</p>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface AdDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {
|
|
124
|
+
fallback?: React.ReactNode;
|
|
184
125
|
}
|
|
185
126
|
|
|
186
|
-
function
|
|
127
|
+
export function AdDescription({ fallback, children, ...props }: AdDescriptionProps) {
|
|
128
|
+
const { data } = useAd();
|
|
129
|
+
const description = getAdDescription(data);
|
|
130
|
+
if (!description) return fallback ? <>{fallback}</> : null;
|
|
131
|
+
return <p {...props}>{children ?? description}</p>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface AdBadgeProps extends React.HTMLAttributes<HTMLSpanElement> {}
|
|
135
|
+
|
|
136
|
+
export function AdBadge({ children, ...props }: AdBadgeProps) {
|
|
137
|
+
const { data } = useAd();
|
|
138
|
+
const type = getAdType(data);
|
|
139
|
+
if (!type) return null;
|
|
140
|
+
const Icon = adCardIcon[type];
|
|
141
|
+
const label = adCardLabel[type];
|
|
187
142
|
return (
|
|
188
|
-
<
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
<SquareDashed className="w-5 h-5 md:w-7 md:h-7" />
|
|
197
|
-
<p className="font-bold text-primary">
|
|
198
|
-
NO AD{" "}
|
|
199
|
-
<span className="text-xs text-muted-foreground/80 font-normal">
|
|
200
|
-
(Your ad here)
|
|
201
|
-
</span>
|
|
202
|
-
</p>
|
|
203
|
-
</div>
|
|
204
|
-
</BasicAdBody>
|
|
143
|
+
<span {...props}>
|
|
144
|
+
{children ?? (
|
|
145
|
+
<>
|
|
146
|
+
{Icon && <Icon className="size-3" />}
|
|
147
|
+
{label}
|
|
148
|
+
</>
|
|
149
|
+
)}
|
|
150
|
+
</span>
|
|
205
151
|
);
|
|
206
152
|
}
|
|
153
|
+
|
|
154
|
+
export interface AdLabelProps extends React.HTMLAttributes<HTMLSpanElement> {}
|
|
155
|
+
|
|
156
|
+
export function AdLabel({ children, ...props }: AdLabelProps) {
|
|
157
|
+
return <span {...props}>{children ?? "AD"}</span>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ─── State components ────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
export interface AdStatusProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
163
|
+
children?: React.ReactNode;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function AdLoading({ children, ...props }: AdStatusProps) {
|
|
167
|
+
const { isLoading } = useAd();
|
|
168
|
+
if (!isLoading) return null;
|
|
169
|
+
return <div {...props}>{children ?? "Loading..."}</div>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function AdEmpty({ children, ...props }: AdStatusProps) {
|
|
173
|
+
const { isEmpty } = useAd();
|
|
174
|
+
if (!isEmpty) return null;
|
|
175
|
+
return <div {...props}>{children ?? "No ad"}</div>;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function AdError({ children, ...props }: AdStatusProps) {
|
|
179
|
+
const { error, isEmpty } = useAd();
|
|
180
|
+
if (!error || isEmpty) return null;
|
|
181
|
+
return <div {...props}>{children ?? "Error loading ad"}</div>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function AdLoaded({ children, ...props }: AdStatusProps) {
|
|
185
|
+
const { data } = useAd();
|
|
186
|
+
if (!data) return null;
|
|
187
|
+
return <div {...props}>{children}</div>;
|
|
188
|
+
}
|
package/src/fetch.ts
CHANGED
|
@@ -1,28 +1,79 @@
|
|
|
1
|
+
import { SlotsClient, type SlotsChain } from "@0xslots/sdk";
|
|
2
|
+
import { type Address, createPublicClient, http } from "viem";
|
|
3
|
+
import { base, baseSepolia } from "viem/chains";
|
|
4
|
+
|
|
1
5
|
import { AdDataQueryError } from "./types";
|
|
2
|
-
import { adlandApiUrl } from "./utils/constants";
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
|
|
7
|
+
const IPFS_GATEWAY = "https://gateway.pinata.cloud/ipfs/";
|
|
8
|
+
|
|
9
|
+
const viemChains: Record<number, typeof base> = {
|
|
10
|
+
8453: base,
|
|
11
|
+
84532: baseSepolia,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a read-only SlotsClient for a given chain.
|
|
16
|
+
*/
|
|
17
|
+
export function createReadClient(
|
|
18
|
+
chainId: SlotsChain,
|
|
19
|
+
rpcUrl?: string,
|
|
20
|
+
): SlotsClient {
|
|
21
|
+
const chain = viemChains[chainId];
|
|
22
|
+
if (!chain) throw new Error(`Unsupported chain: ${chainId}`);
|
|
23
|
+
|
|
24
|
+
const publicClient = createPublicClient({
|
|
25
|
+
chain,
|
|
26
|
+
transport: http(rpcUrl),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return new SlotsClient({ chainId, publicClient });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Fetch ad content from a metadata URI (IPFS or HTTP)
|
|
34
|
+
*/
|
|
35
|
+
export const fetchAdFromURI = async (uri: string) => {
|
|
36
|
+
if (!uri) throw new Error(AdDataQueryError.NO_AD);
|
|
37
|
+
|
|
38
|
+
const url = uri.startsWith("ipfs://")
|
|
39
|
+
? `${IPFS_GATEWAY}${uri.slice(7)}`
|
|
40
|
+
: uri;
|
|
6
41
|
|
|
7
42
|
const res = await fetch(url, {
|
|
8
43
|
method: "GET",
|
|
9
|
-
headers: {
|
|
10
|
-
"Content-Type": "application/json",
|
|
11
|
-
},
|
|
44
|
+
headers: { Accept: "application/json" },
|
|
12
45
|
});
|
|
13
46
|
|
|
14
47
|
if (!res.ok) {
|
|
15
|
-
if (res.status === 404)
|
|
16
|
-
throw new Error(AdDataQueryError.NO_AD);
|
|
17
|
-
}
|
|
48
|
+
if (res.status === 404) throw new Error(AdDataQueryError.NO_AD);
|
|
18
49
|
throw new Error(AdDataQueryError.ERROR);
|
|
19
50
|
}
|
|
20
51
|
|
|
21
52
|
const data = await res.json();
|
|
53
|
+
if (data.error) throw new Error(data.error);
|
|
22
54
|
|
|
23
|
-
|
|
24
|
-
|
|
55
|
+
return data;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Fetch the metadata URI for a slot using the SDK.
|
|
60
|
+
*/
|
|
61
|
+
export const fetchMetadataURI = async (
|
|
62
|
+
client: SlotsClient,
|
|
63
|
+
slotAddress: string,
|
|
64
|
+
): Promise<string> => {
|
|
65
|
+
const info = await client.getSlotInfo(slotAddress as Address);
|
|
66
|
+
const moduleAddress = (info as { module: Address }).module;
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
!moduleAddress ||
|
|
70
|
+
moduleAddress === "0x0000000000000000000000000000000000000000"
|
|
71
|
+
) {
|
|
72
|
+
return "";
|
|
25
73
|
}
|
|
26
74
|
|
|
27
|
-
return
|
|
75
|
+
return client.modules.metadata.getURI(
|
|
76
|
+
moduleAddress,
|
|
77
|
+
slotAddress as Address,
|
|
78
|
+
);
|
|
28
79
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AdData } from "@adland/data";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
|
|
4
|
+
export interface AdContextValue {
|
|
5
|
+
data: AdData | null;
|
|
6
|
+
isLoading: boolean;
|
|
7
|
+
error: unknown;
|
|
8
|
+
isEmpty: boolean;
|
|
9
|
+
slot?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const AdContext = createContext<AdContextValue | null>(null);
|
|
13
|
+
|
|
14
|
+
export function useAd(): AdContextValue {
|
|
15
|
+
const ctx = useContext(AdContext);
|
|
16
|
+
if (!ctx) throw new Error("useAd must be used within an <Ad> component");
|
|
17
|
+
return ctx;
|
|
18
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,35 @@
|
|
|
1
|
-
//
|
|
2
|
-
export { Ad } from "./components/Ad";
|
|
3
|
-
export type { AdProps, AdDataQueryResponse, AdDataQueryError } from "./types";
|
|
4
|
-
|
|
5
|
-
// Utility exports
|
|
1
|
+
// Compound Ad components
|
|
6
2
|
export {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
Ad,
|
|
4
|
+
AdBadge,
|
|
5
|
+
AdDescription,
|
|
6
|
+
AdEmpty,
|
|
7
|
+
AdError,
|
|
8
|
+
AdImage,
|
|
9
|
+
AdLabel,
|
|
10
|
+
AdLoaded,
|
|
11
|
+
AdLoading,
|
|
12
|
+
AdTitle,
|
|
13
|
+
} from "./components/Ad";
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
AdBadgeProps,
|
|
17
|
+
AdDescriptionProps,
|
|
18
|
+
AdImageProps,
|
|
19
|
+
AdLabelProps,
|
|
20
|
+
AdStatusProps,
|
|
21
|
+
AdTitleProps,
|
|
22
|
+
} from "./components/Ad";
|
|
23
|
+
|
|
24
|
+
// Context hook
|
|
25
|
+
export { useAd } from "./hooks/useAdContext";
|
|
26
|
+
export type { AdContextValue } from "./hooks/useAdContext";
|
|
27
|
+
|
|
28
|
+
// Field helpers
|
|
29
|
+
export { getAdDescription, getAdImage, getAdTitle, getAdType } from "./utils/ad-fields";
|
|
30
|
+
|
|
31
|
+
// Types
|
|
32
|
+
export type { AdProps, AdDataQueryError } from "./types";
|
|
11
33
|
|
|
12
|
-
// Constants
|
|
13
|
-
export
|
|
34
|
+
// Constants
|
|
35
|
+
export { adCardIcon, adCardLabel } from "./utils/constants";
|
package/src/types.ts
CHANGED
|
@@ -1,32 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export type Network = "testnet" | "mainnet";
|
|
1
|
+
import type { SlotsChain } from "@0xslots/sdk";
|
|
2
|
+
import type { AdData } from "@adland/data";
|
|
4
3
|
|
|
5
4
|
export interface AdProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
6
5
|
/**
|
|
7
|
-
* The
|
|
6
|
+
* The slot contract address (0xSlots v3).
|
|
7
|
+
* Required when fetching from chain. Omit when passing static `data`.
|
|
8
|
+
*/
|
|
9
|
+
slot?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Static ad data. When provided, skips on-chain fetching.
|
|
8
12
|
*/
|
|
9
|
-
|
|
13
|
+
data?: AdData;
|
|
10
14
|
/**
|
|
11
|
-
*
|
|
15
|
+
* Chain ID for on-chain reads. Defaults to BASE (8453).
|
|
12
16
|
*/
|
|
13
|
-
|
|
17
|
+
chainId?: SlotsChain;
|
|
14
18
|
/**
|
|
15
|
-
*
|
|
19
|
+
* Optional RPC URL override. If not provided, uses public RPC for the chain.
|
|
16
20
|
*/
|
|
17
|
-
|
|
21
|
+
rpcUrl?: string;
|
|
18
22
|
/**
|
|
19
|
-
*
|
|
23
|
+
* Compound children (AdImage, AdTitle, etc.)
|
|
20
24
|
*/
|
|
21
|
-
|
|
25
|
+
children?: React.ReactNode;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export enum AdDataQueryError {
|
|
25
29
|
NO_AD = "NO_AD",
|
|
26
30
|
ERROR = "ERROR",
|
|
27
31
|
}
|
|
28
|
-
|
|
29
|
-
export interface AdDataQueryResponse {
|
|
30
|
-
error?: AdDataQueryError;
|
|
31
|
-
data?: AdData;
|
|
32
|
-
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AdData } from "@adland/data";
|
|
2
|
+
import sdk from "@farcaster/miniapp-sdk";
|
|
3
|
+
|
|
4
|
+
export function performAdAction(adData: AdData) {
|
|
5
|
+
try {
|
|
6
|
+
switch (adData.type) {
|
|
7
|
+
case "link":
|
|
8
|
+
sdk.actions.openUrl(adData.data.url);
|
|
9
|
+
break;
|
|
10
|
+
case "cast":
|
|
11
|
+
sdk.actions.viewCast({ hash: adData.data.hash });
|
|
12
|
+
break;
|
|
13
|
+
case "miniapp":
|
|
14
|
+
sdk.actions.openMiniApp({ url: adData.data.url });
|
|
15
|
+
break;
|
|
16
|
+
case "token":
|
|
17
|
+
sdk.actions.viewToken({ token: adData.data.address });
|
|
18
|
+
break;
|
|
19
|
+
case "farcasterProfile":
|
|
20
|
+
sdk.actions.viewProfile({ fid: Number.parseInt(adData.data.fid, 10) });
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error("[@adland/react] Failed to perform ad action:", err);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AdData, AdType } from "@adland/data";
|
|
2
|
+
|
|
3
|
+
const IMAGE_KEYS = ["image", "icon", "pfpUrl", "logoURI", "imageUrl"] as const;
|
|
4
|
+
const TITLE_KEYS = ["title", "displayName", "username", "name", "symbol"] as const;
|
|
5
|
+
const DESC_KEYS = ["description", "bio", "text", "name"] as const;
|
|
6
|
+
|
|
7
|
+
function flatFields(data: AdData): Record<string, unknown> {
|
|
8
|
+
return { ...data.data, ...(data.metadata ?? {}) };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getAdImage(data: AdData | null): string | null {
|
|
12
|
+
if (!data) return null;
|
|
13
|
+
const fields = flatFields(data);
|
|
14
|
+
for (const key of IMAGE_KEYS) {
|
|
15
|
+
const v = fields[key];
|
|
16
|
+
if (typeof v === "string" && v) return v;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getAdTitle(data: AdData | null): string | null {
|
|
22
|
+
if (!data) return null;
|
|
23
|
+
const fields = flatFields(data);
|
|
24
|
+
for (const key of TITLE_KEYS) {
|
|
25
|
+
const v = fields[key];
|
|
26
|
+
if (typeof v === "string" && v) return v;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getAdDescription(data: AdData | null): string | null {
|
|
32
|
+
if (!data) return null;
|
|
33
|
+
const fields = flatFields(data);
|
|
34
|
+
const title = getAdTitle(data);
|
|
35
|
+
for (const key of DESC_KEYS) {
|
|
36
|
+
const v = fields[key];
|
|
37
|
+
if (typeof v === "string" && v && v !== title) return v;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getAdType(data: AdData | null): AdType | null {
|
|
43
|
+
if (!data) return null;
|
|
44
|
+
return data.type as AdType;
|
|
45
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2024 dan5py (git@dan5py.com)
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
const BasicAdBody = ({
|
|
2
|
-
name,
|
|
3
|
-
children,
|
|
4
|
-
...rest
|
|
5
|
-
}: {
|
|
6
|
-
children: React.ReactNode;
|
|
7
|
-
name?: string;
|
|
8
|
-
} & React.HTMLAttributes<HTMLDivElement>) => {
|
|
9
|
-
return (
|
|
10
|
-
<div
|
|
11
|
-
className="border p-2 md:p-4 flex hover:cursor-pointer hover:bg-primary/10 flex-row rounded-md justify-between items-center gap-2 relative border-b-2 border-b-primary"
|
|
12
|
-
{...rest}
|
|
13
|
-
>
|
|
14
|
-
{children}
|
|
15
|
-
<div className="flex flex-row items-center border-2 border-primary">
|
|
16
|
-
{name && (
|
|
17
|
-
<div className="text-xs font-semibold text-primary px-1">{name}</div>
|
|
18
|
-
)}
|
|
19
|
-
<div className="inline-flex items-center border p-1 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent bg-primary text-primary-foreground hover:bg-primary/80">
|
|
20
|
-
AD
|
|
21
|
-
</div>
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
24
|
-
);
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export default BasicAdBody;
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { adCardIcon } from "../../utils/constants";
|
|
2
|
-
import { formatRelativeTime } from "../../utils/relativeTime";
|
|
3
|
-
import sdk from "@farcaster/miniapp-sdk";
|
|
4
|
-
import BasicAdBody from "../BasicAdBody";
|
|
5
|
-
import { CastAd } from "@adland/data";
|
|
6
|
-
|
|
7
|
-
// Cast Ad Component
|
|
8
|
-
const CastAdContent = ({ data }: { data: CastAd }) => {
|
|
9
|
-
const { data: castData, metadata: castMetadata } = data;
|
|
10
|
-
const Icon = adCardIcon["cast"];
|
|
11
|
-
return (
|
|
12
|
-
<BasicAdBody
|
|
13
|
-
name={"CAST"}
|
|
14
|
-
onClick={() => {
|
|
15
|
-
sdk.actions.viewCast({
|
|
16
|
-
hash: castData.hash,
|
|
17
|
-
});
|
|
18
|
-
}}
|
|
19
|
-
>
|
|
20
|
-
<div className="flex flex-row items-center gap-2">
|
|
21
|
-
{castMetadata?.pfpUrl ? (
|
|
22
|
-
<img
|
|
23
|
-
src={castMetadata.pfpUrl}
|
|
24
|
-
alt="Cast pfp"
|
|
25
|
-
className="w-10 h-10 rounded-md"
|
|
26
|
-
/>
|
|
27
|
-
) : (
|
|
28
|
-
<Icon className="w-5 h-5 md:w-7 md:h-7" />
|
|
29
|
-
)}
|
|
30
|
-
<div className="flex flex-col gap-0">
|
|
31
|
-
{castMetadata?.username ? (
|
|
32
|
-
<div className="flex flex-row items-center gap-1">
|
|
33
|
-
<p className="font-bold text-primary">{castMetadata.username}</p>
|
|
34
|
-
{castMetadata.timestamp && (
|
|
35
|
-
<p className="text-xs text-muted-foreground/80">
|
|
36
|
-
{formatRelativeTime(
|
|
37
|
-
new Date(castMetadata.timestamp).getTime(),
|
|
38
|
-
)}
|
|
39
|
-
</p>
|
|
40
|
-
)}
|
|
41
|
-
</div>
|
|
42
|
-
) : (
|
|
43
|
-
<p className="font-bold text-primary">CAST</p>
|
|
44
|
-
)}
|
|
45
|
-
{castMetadata?.text ? (
|
|
46
|
-
<p className="text-black">{castMetadata.text}</p>
|
|
47
|
-
) : (
|
|
48
|
-
<p className="text-muted-foreground/80">cast</p>
|
|
49
|
-
)}
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
</BasicAdBody>
|
|
53
|
-
);
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
export default CastAdContent;
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { FarcasterProfileAd } from "@adland/data";
|
|
2
|
-
import sdk from "@farcaster/miniapp-sdk";
|
|
3
|
-
import BasicAdBody from "../BasicAdBody";
|
|
4
|
-
import { adCardIcon } from "../../utils/constants";
|
|
5
|
-
|
|
6
|
-
// Farcaster Profile Ad Component
|
|
7
|
-
const FarcasterProfileAdContent = ({ data }: { data: FarcasterProfileAd }) => {
|
|
8
|
-
const Icon = adCardIcon["farcasterProfile"];
|
|
9
|
-
const { data: profileData, metadata: profileMetadata } = data;
|
|
10
|
-
const fid = profileData.fid;
|
|
11
|
-
|
|
12
|
-
return (
|
|
13
|
-
<BasicAdBody
|
|
14
|
-
name={"PROFILE"}
|
|
15
|
-
onClick={() => {
|
|
16
|
-
sdk.actions.viewProfile({ fid: parseInt(fid) });
|
|
17
|
-
}}
|
|
18
|
-
>
|
|
19
|
-
<div className="flex flex-row items-center gap-2 flex-1 min-w-0">
|
|
20
|
-
{profileMetadata?.pfpUrl ? (
|
|
21
|
-
<img
|
|
22
|
-
src={profileMetadata.pfpUrl}
|
|
23
|
-
alt={profileMetadata.username || "Profile picture"}
|
|
24
|
-
className="w-10 h-10 md:w-12 md:h-12 rounded-full object-cover flex-shrink-0"
|
|
25
|
-
/>
|
|
26
|
-
) : (
|
|
27
|
-
<Icon className="w-5 h-5 md:w-7 md:h-7 flex-shrink-0" />
|
|
28
|
-
)}
|
|
29
|
-
<div className="flex flex-col gap-1 min-w-0 flex-1">
|
|
30
|
-
{!Boolean(profileMetadata) && (
|
|
31
|
-
<div className="flex flex-row items-center gap-2">
|
|
32
|
-
<Icon className="w-5 h-5 md:w-7 md:h-7 flex-shrink-0" />
|
|
33
|
-
<p className="font-bold text-primary">PROFILE</p>
|
|
34
|
-
</div>
|
|
35
|
-
)}
|
|
36
|
-
<div className="flex flex-row items-center gap-1">
|
|
37
|
-
<p className="text-sm text-primary truncate">
|
|
38
|
-
<span className="font-bold">{profileMetadata?.displayName} </span>
|
|
39
|
-
<span className="text-xs font-light text-muted-foreground/80">
|
|
40
|
-
@{profileMetadata?.username}
|
|
41
|
-
</span>
|
|
42
|
-
</p>
|
|
43
|
-
{profileMetadata?.pro && (
|
|
44
|
-
<span className="text-xs bg-primary text-primary-foreground px-1 rounded">
|
|
45
|
-
PRO
|
|
46
|
-
</span>
|
|
47
|
-
)}
|
|
48
|
-
</div>
|
|
49
|
-
{profileMetadata?.bio ? (
|
|
50
|
-
<p className="text-xs text-muted-foreground/80 line-clamp-1">
|
|
51
|
-
{profileMetadata.bio}
|
|
52
|
-
</p>
|
|
53
|
-
) : (
|
|
54
|
-
(profileMetadata?.followers !== undefined ||
|
|
55
|
-
profileMetadata?.following !== undefined) && (
|
|
56
|
-
<p className="text-xs text-muted-foreground/80">
|
|
57
|
-
{profileMetadata.followers !== undefined && (
|
|
58
|
-
<span>
|
|
59
|
-
{profileMetadata.followers.toLocaleString()} followers
|
|
60
|
-
</span>
|
|
61
|
-
)}
|
|
62
|
-
{profileMetadata.followers !== undefined &&
|
|
63
|
-
profileMetadata.following !== undefined &&
|
|
64
|
-
" • "}
|
|
65
|
-
{profileMetadata.following !== undefined && (
|
|
66
|
-
<span>
|
|
67
|
-
{profileMetadata.following.toLocaleString()} following
|
|
68
|
-
</span>
|
|
69
|
-
)}
|
|
70
|
-
</p>
|
|
71
|
-
)
|
|
72
|
-
)}
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
75
|
-
</BasicAdBody>
|
|
76
|
-
);
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
export default FarcasterProfileAdContent;
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import sdk from "@farcaster/miniapp-sdk";
|
|
2
|
-
import BasicAdBody from "../BasicAdBody";
|
|
3
|
-
import { adCardIcon } from "../../utils/constants";
|
|
4
|
-
import { LinkAd } from "@adland/data";
|
|
5
|
-
|
|
6
|
-
// Link Ad Component
|
|
7
|
-
const LinkAdContent = ({ data }: { data: LinkAd }) => {
|
|
8
|
-
const Icon = adCardIcon["link"];
|
|
9
|
-
const { data: linkData, metadata: linkMetadata } = data;
|
|
10
|
-
|
|
11
|
-
return (
|
|
12
|
-
<BasicAdBody
|
|
13
|
-
name={"LINK"}
|
|
14
|
-
onClick={() => {
|
|
15
|
-
sdk.actions.openUrl(linkData.url);
|
|
16
|
-
}}
|
|
17
|
-
>
|
|
18
|
-
<div className="flex flex-row items-center gap-2 flex-1 min-w-0">
|
|
19
|
-
{linkMetadata?.image && (
|
|
20
|
-
<img
|
|
21
|
-
src={linkMetadata.image}
|
|
22
|
-
alt={linkMetadata.title || "Link preview"}
|
|
23
|
-
className="w-10 h-10 md:w-12 md:h-12 rounded object-cover flex-shrink-0"
|
|
24
|
-
/>
|
|
25
|
-
)}
|
|
26
|
-
<div className="flex flex-col gap-1 min-w-0 flex-1">
|
|
27
|
-
{!Boolean(linkMetadata) && (
|
|
28
|
-
<div className="flex flex-row items-center gap-2">
|
|
29
|
-
<Icon className="w-5 h-5 md:w-7 md:h-7 flex-shrink-0" />
|
|
30
|
-
<p className="font-bold text-primary">LINK</p>
|
|
31
|
-
</div>
|
|
32
|
-
)}
|
|
33
|
-
{linkMetadata?.title && (
|
|
34
|
-
<p className="text-sm text-muted-foreground truncate">
|
|
35
|
-
{linkMetadata.title}
|
|
36
|
-
</p>
|
|
37
|
-
)}
|
|
38
|
-
{linkMetadata?.description && (
|
|
39
|
-
<p className="text-xs text-muted-foreground/80 line-clamp-1">
|
|
40
|
-
{linkMetadata.description}
|
|
41
|
-
</p>
|
|
42
|
-
)}
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
</BasicAdBody>
|
|
46
|
-
);
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export default LinkAdContent;
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { MiniAppAd } from "@adland/data";
|
|
2
|
-
import sdk from "@farcaster/miniapp-sdk";
|
|
3
|
-
import BasicAdBody from "../BasicAdBody";
|
|
4
|
-
import { adCardIcon } from "../../utils/constants";
|
|
5
|
-
|
|
6
|
-
const MiniappAdContent = ({ data }: { data: MiniAppAd }) => {
|
|
7
|
-
const Icon = adCardIcon["miniapp"];
|
|
8
|
-
const { data: miniappData, metadata: miniappMetadata } = data;
|
|
9
|
-
console.log({ miniappMetadata });
|
|
10
|
-
return (
|
|
11
|
-
<BasicAdBody
|
|
12
|
-
name={"MINIAPP"}
|
|
13
|
-
onClick={() => {
|
|
14
|
-
sdk.actions.openMiniApp({
|
|
15
|
-
url: miniappData.url,
|
|
16
|
-
});
|
|
17
|
-
}}
|
|
18
|
-
>
|
|
19
|
-
<div className="flex flex-row items-center gap-2">
|
|
20
|
-
{miniappMetadata?.icon ? (
|
|
21
|
-
<img
|
|
22
|
-
src={miniappMetadata.icon}
|
|
23
|
-
alt="Miniapp icon"
|
|
24
|
-
className="w-10 h-10 rounded-md"
|
|
25
|
-
/>
|
|
26
|
-
) : (
|
|
27
|
-
<Icon className="w-5 h-5 md:w-7 md:h-7" />
|
|
28
|
-
)}
|
|
29
|
-
<div className="flex flex-col gap-0">
|
|
30
|
-
{miniappMetadata?.title ? (
|
|
31
|
-
<p className="font-bold text-primary">{miniappMetadata.title}</p>
|
|
32
|
-
) : (
|
|
33
|
-
<p className="font-bold text-primary">MINIAPP</p>
|
|
34
|
-
)}
|
|
35
|
-
{miniappMetadata?.description ? (
|
|
36
|
-
<p className="text-xs text-muted-foreground/80">
|
|
37
|
-
{miniappMetadata.description}
|
|
38
|
-
</p>
|
|
39
|
-
) : (
|
|
40
|
-
<p className="text-xs text-muted-foreground/80">miniapp</p>
|
|
41
|
-
)}
|
|
42
|
-
</div>
|
|
43
|
-
</div>
|
|
44
|
-
</BasicAdBody>
|
|
45
|
-
);
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export default MiniappAdContent;
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { TokenAd } from "@adland/data";
|
|
2
|
-
import sdk from "@farcaster/miniapp-sdk";
|
|
3
|
-
import BasicAdBody from "../BasicAdBody";
|
|
4
|
-
import { adCardIcon } from "../../utils/constants";
|
|
5
|
-
|
|
6
|
-
const TokenAdContent = ({ data }: { data: TokenAd }) => {
|
|
7
|
-
const Icon = adCardIcon["token"];
|
|
8
|
-
const { data: tokenData, metadata: tokenMetadata } = data;
|
|
9
|
-
|
|
10
|
-
return (
|
|
11
|
-
<BasicAdBody
|
|
12
|
-
name={"TOKEN"}
|
|
13
|
-
onClick={() => {
|
|
14
|
-
sdk.actions.viewToken({ token: tokenData.address });
|
|
15
|
-
}}
|
|
16
|
-
>
|
|
17
|
-
<div className="flex flex-row items-center gap-2 flex-1 min-w-0">
|
|
18
|
-
{tokenMetadata?.logoURI ? (
|
|
19
|
-
<img
|
|
20
|
-
src={tokenMetadata.logoURI}
|
|
21
|
-
alt={tokenMetadata.name || "Token logo"}
|
|
22
|
-
className="w-10 h-10 md:w-12 md:h-12 rounded object-cover flex-shrink-0"
|
|
23
|
-
/>
|
|
24
|
-
) : (
|
|
25
|
-
<Icon className="w-5 h-5 md:w-7 md:h-7 flex-shrink-0" />
|
|
26
|
-
)}
|
|
27
|
-
<div className="flex flex-col gap-1 min-w-0 flex-1">
|
|
28
|
-
{!Boolean(tokenMetadata) && (
|
|
29
|
-
<div className="flex flex-row items-center gap-2">
|
|
30
|
-
<Icon className="w-5 h-5 md:w-7 md:h-7 flex-shrink-0" />
|
|
31
|
-
<p className="font-bold text-primary">TOKEN</p>
|
|
32
|
-
</div>
|
|
33
|
-
)}
|
|
34
|
-
{tokenMetadata?.symbol ? (
|
|
35
|
-
<p className="text-sm font-bold text-primary truncate">
|
|
36
|
-
${tokenMetadata.symbol.toUpperCase()}
|
|
37
|
-
</p>
|
|
38
|
-
) : (
|
|
39
|
-
<p className="text-sm font-bold text-primary">TOKEN</p>
|
|
40
|
-
)}
|
|
41
|
-
{tokenMetadata?.name ? (
|
|
42
|
-
<p className="text-xs text-muted-foreground/80 line-clamp-1">
|
|
43
|
-
{tokenMetadata.name}
|
|
44
|
-
</p>
|
|
45
|
-
) : (
|
|
46
|
-
<p className="text-xs text-muted-foreground/80 line-clamp-1">
|
|
47
|
-
{tokenData.address.slice(0, 6)}...{tokenData.address.slice(-4)}
|
|
48
|
-
</p>
|
|
49
|
-
)}
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
</BasicAdBody>
|
|
53
|
-
);
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
export default TokenAdContent;
|
package/src/utils/index.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { Network } from "../types";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Get the base URL for the network
|
|
5
|
-
* Uses relative URL in browser (for local dev), otherwise defaults to production
|
|
6
|
-
*/
|
|
7
|
-
export const getBaseUrl = (network: Network): string => {
|
|
8
|
-
if (typeof window !== "undefined") {
|
|
9
|
-
// In browser - use relative URL for local development
|
|
10
|
-
return "";
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
if (network === "testnet") {
|
|
14
|
-
return "https://testnet.adland.space";
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (network === "mainnet") {
|
|
18
|
-
return "https://app.adland.space";
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return "";
|
|
22
|
-
};
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Formats a timestamp as a relative time string (e.g., "2 days ago", "55 minutes ago")
|
|
3
|
-
* @param timestamp - Unix timestamp in seconds or milliseconds
|
|
4
|
-
* @returns Formatted relative time string
|
|
5
|
-
*/
|
|
6
|
-
export function formatRelativeTime(timestamp: number): string {
|
|
7
|
-
// Convert to milliseconds if timestamp is in seconds (less than year 2000 in ms)
|
|
8
|
-
const timestampMs = timestamp < 946684800000 ? timestamp * 1000 : timestamp;
|
|
9
|
-
const now = Date.now();
|
|
10
|
-
const diffMs = now - timestampMs;
|
|
11
|
-
const diffSeconds = Math.floor(diffMs / 1000);
|
|
12
|
-
const diffMinutes = Math.floor(diffSeconds / 60);
|
|
13
|
-
const diffHours = Math.floor(diffMinutes / 60);
|
|
14
|
-
const diffDays = Math.floor(diffHours / 24);
|
|
15
|
-
const diffWeeks = Math.floor(diffDays / 7);
|
|
16
|
-
const diffMonths = Math.floor(diffDays / 30);
|
|
17
|
-
const diffYears = Math.floor(diffDays / 365);
|
|
18
|
-
|
|
19
|
-
if (diffSeconds < 60) {
|
|
20
|
-
return diffSeconds <= 1 ? "just now" : `${diffSeconds} seconds ago`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (diffMinutes < 60) {
|
|
24
|
-
return diffMinutes === 1 ? "1 minute ago" : `${diffMinutes} minutes ago`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (diffHours < 24) {
|
|
28
|
-
return diffHours === 1 ? "1 hour ago" : `${diffHours} hours ago`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (diffDays < 7) {
|
|
32
|
-
return diffDays === 1 ? "1 day ago" : `${diffDays} days ago`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (diffWeeks < 4) {
|
|
36
|
-
return diffWeeks === 1 ? "1 week ago" : `${diffWeeks} weeks ago`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (diffMonths < 12) {
|
|
40
|
-
return diffMonths === 1 ? "1 month ago" : `${diffMonths} months ago`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return diffYears === 1 ? "1 year ago" : `${diffYears} years ago`;
|
|
44
|
-
}
|
package/src/utils/sdk.ts
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import { sdk } from "@farcaster/miniapp-sdk";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Checks if the SDK is ready and initialized
|
|
5
|
-
*/
|
|
6
|
-
export function isSdkReady(): boolean {
|
|
7
|
-
try {
|
|
8
|
-
// Check if SDK exists
|
|
9
|
-
if (!sdk) {
|
|
10
|
-
console.error("[@adland/react] SDK is not available");
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// Check if quickAuth is available
|
|
15
|
-
if (!sdk.quickAuth) {
|
|
16
|
-
console.error(
|
|
17
|
-
"[@adland/react] SDK quickAuth is not available. Make sure the SDK is properly initialized.",
|
|
18
|
-
);
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return true;
|
|
23
|
-
} catch (error) {
|
|
24
|
-
console.error("[@adland/react] Error checking SDK readiness:", error);
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Sends a tracking request using the SDK's quickAuth
|
|
31
|
-
*/
|
|
32
|
-
export async function sendTrackRequest(
|
|
33
|
-
url: string,
|
|
34
|
-
data: {
|
|
35
|
-
type: "view" | "click";
|
|
36
|
-
adOwner: string;
|
|
37
|
-
adId: string;
|
|
38
|
-
},
|
|
39
|
-
): Promise<Response> {
|
|
40
|
-
if (!isSdkReady()) {
|
|
41
|
-
console.error(
|
|
42
|
-
"[@adland/react] SDK is not ready. Cannot send tracking request.",
|
|
43
|
-
);
|
|
44
|
-
return Promise.reject(
|
|
45
|
-
new Error(
|
|
46
|
-
"[@adland/react] SDK is not ready. Cannot send tracking request.",
|
|
47
|
-
),
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
if (!sdk.quickAuth) {
|
|
53
|
-
throw new Error("[@adland/react] SDK quickAuth is not available");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const requestOptions = {
|
|
57
|
-
method: "POST",
|
|
58
|
-
headers: {
|
|
59
|
-
"Content-Type": "application/json",
|
|
60
|
-
"x-auth-type": "farcaster",
|
|
61
|
-
},
|
|
62
|
-
body: JSON.stringify(data),
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// Log request details for debugging
|
|
66
|
-
console.log("[@adland/react] Sending track request:", {
|
|
67
|
-
url,
|
|
68
|
-
method: requestOptions.method,
|
|
69
|
-
headers: requestOptions.headers,
|
|
70
|
-
body: data,
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
const response = await sdk.quickAuth.fetch(url, requestOptions);
|
|
74
|
-
|
|
75
|
-
// Check if response is ok
|
|
76
|
-
if (!response.ok) {
|
|
77
|
-
const statusText = response.statusText;
|
|
78
|
-
const status = response.status;
|
|
79
|
-
|
|
80
|
-
// Try to get error body if available
|
|
81
|
-
let errorBody = null;
|
|
82
|
-
try {
|
|
83
|
-
const contentType = response.headers.get("content-type");
|
|
84
|
-
if (contentType?.includes("application/json")) {
|
|
85
|
-
errorBody = await response.clone().json();
|
|
86
|
-
} else {
|
|
87
|
-
errorBody = await response.clone().text();
|
|
88
|
-
}
|
|
89
|
-
} catch (e) {
|
|
90
|
-
// Ignore errors when reading response body
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
console.error("[@adland/react] Track request failed:", {
|
|
94
|
-
url,
|
|
95
|
-
status,
|
|
96
|
-
statusText,
|
|
97
|
-
errorBody,
|
|
98
|
-
requestBody: data,
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
throw new Error(
|
|
102
|
-
`[@adland/react] Track request failed: ${status} ${statusText}${errorBody ? ` - ${JSON.stringify(errorBody)}` : ""}`,
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return response;
|
|
107
|
-
} catch (error) {
|
|
108
|
-
// Enhanced error logging
|
|
109
|
-
if (error instanceof Error) {
|
|
110
|
-
console.error("[@adland/react] Error sending track request:", {
|
|
111
|
-
message: error.message,
|
|
112
|
-
name: error.name,
|
|
113
|
-
stack: error.stack,
|
|
114
|
-
url,
|
|
115
|
-
requestData: data,
|
|
116
|
-
});
|
|
117
|
-
} else {
|
|
118
|
-
console.error("[@adland/react] Unknown error sending track request:", {
|
|
119
|
-
error,
|
|
120
|
-
url,
|
|
121
|
-
requestData: data,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
throw error;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Checks if SDK actions are ready
|
|
130
|
-
*/
|
|
131
|
-
export async function checkSdkActionsReady(): Promise<boolean> {
|
|
132
|
-
try {
|
|
133
|
-
if (!sdk) {
|
|
134
|
-
console.error("[@adland/react] SDK is not available");
|
|
135
|
-
return false;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (!sdk.actions) {
|
|
139
|
-
console.error("[@adland/react] SDK actions are not available");
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Check if ready() function exists
|
|
144
|
-
if (typeof sdk.actions.ready !== "function") {
|
|
145
|
-
console.error(
|
|
146
|
-
"[@adland/react] SDK actions.ready() is not available. Make sure the SDK is properly initialized.",
|
|
147
|
-
);
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return true;
|
|
152
|
-
} catch (error) {
|
|
153
|
-
console.error(
|
|
154
|
-
"[@adland/react] Error checking SDK actions readiness:",
|
|
155
|
-
error,
|
|
156
|
-
);
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
}
|