@adland/react 0.13.0 → 0.14.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/CHANGELOG.md +15 -0
- package/package.json +4 -2
- package/src/components/Ad.tsx +145 -135
- package/src/fetch.ts +40 -41
- package/src/hooks/useAdContext.ts +21 -0
- package/src/index.ts +33 -11
- package/src/types.ts +19 -18
- package/src/utils/ad-actions.ts +59 -0
- package/src/utils/ad-fields.ts +45 -0
- 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 -158
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @adland/react
|
|
2
2
|
|
|
3
|
+
## 0.14.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- bd79045: update empty state & props
|
|
8
|
+
|
|
9
|
+
## 0.13.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 71a90f5: Export AdContent component for rendering ad data without on-chain fetching
|
|
14
|
+
- 8877133: refactor
|
|
15
|
+
- Updated dependencies [8877133]
|
|
16
|
+
- @adland/data@0.14.1
|
|
17
|
+
|
|
3
18
|
## 0.13.0
|
|
4
19
|
|
|
5
20
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adland/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
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
|
+
"@0xslots/sdk": "0.10.1",
|
|
38
|
+
"@adland/data": "0.14.1"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
39
41
|
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
package/src/components/Ad.tsx
CHANGED
|
@@ -1,185 +1,195 @@
|
|
|
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 { fetchAdFromURI, fetchMetadataURI } from "../fetch";
|
|
16
|
-
|
|
17
|
-
const DEFAULT_RPC = "https://sepolia.base.org";
|
|
18
|
-
const DEFAULT_METADATA_MODULE = "0x6c5A8A7f061bEd94b1b88CFAd4e1a1a8C5c4e527";
|
|
8
|
+
import { AdDataQueryError, type AdProps } from "../types";
|
|
9
|
+
import { performAdAction, performEmptyAdAction } 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 ──────────────────────────────────────────────────────────
|
|
19
14
|
|
|
20
15
|
/**
|
|
21
|
-
* Ad component
|
|
22
|
-
* Fetches ad content URI from on-chain, then fetches content from IPFS/HTTP.
|
|
16
|
+
* Root Ad component — compound pattern.
|
|
23
17
|
*
|
|
24
18
|
* @example
|
|
25
19
|
* ```tsx
|
|
26
|
-
* <Ad slot="0xabc...123"
|
|
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 />
|
|
25
|
+
* </Ad>
|
|
27
26
|
* ```
|
|
28
27
|
*/
|
|
29
28
|
export function Ad({
|
|
30
29
|
slot,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
data: staticData,
|
|
31
|
+
chainId = SlotsChain.BASE,
|
|
32
|
+
rpcUrl,
|
|
33
|
+
baseLinkUrl = "https://app.0xslots.org",
|
|
34
|
+
children,
|
|
35
35
|
...props
|
|
36
36
|
}: AdProps) {
|
|
37
37
|
const ref = useRef<HTMLDivElement>(null);
|
|
38
|
-
|
|
38
|
+
|
|
39
|
+
const client = useMemo(
|
|
40
|
+
() => (slot ? createReadClient(chainId, rpcUrl) : null),
|
|
41
|
+
[slot, chainId, rpcUrl],
|
|
42
|
+
);
|
|
39
43
|
|
|
40
44
|
const {
|
|
41
|
-
data:
|
|
45
|
+
data: fetchedData,
|
|
42
46
|
isLoading,
|
|
43
47
|
error,
|
|
44
48
|
} = useFetch<AdData>(
|
|
45
49
|
`ad-data-${slot}`,
|
|
46
50
|
async () => {
|
|
47
|
-
|
|
51
|
+
if (!client || !slot) throw new Error(AdDataQueryError.NO_AD);
|
|
52
|
+
const uri = await fetchMetadataURI(client, slot);
|
|
48
53
|
if (!uri) throw new Error(AdDataQueryError.NO_AD);
|
|
49
54
|
return fetchAdFromURI(uri);
|
|
50
55
|
},
|
|
51
|
-
{ enabled: !!slot },
|
|
56
|
+
{ enabled: !!slot && !staticData },
|
|
52
57
|
);
|
|
53
58
|
|
|
54
|
-
const
|
|
55
|
-
(type: "view" | "click") => {
|
|
56
|
-
const trackEndpoint = `${networkBaseUrl}/api/analytics/track`;
|
|
57
|
-
|
|
58
|
-
sendTrackRequest(trackEndpoint, {
|
|
59
|
-
type,
|
|
60
|
-
slot,
|
|
61
|
-
}).catch((error: unknown) => {
|
|
62
|
-
console.error(`[@adland/react] Failed to track ${type}:`, error);
|
|
63
|
-
});
|
|
64
|
-
},
|
|
65
|
-
[slot, networkBaseUrl],
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
useEffect(() => {
|
|
69
|
-
const el = ref.current;
|
|
70
|
-
if (!el) return;
|
|
71
|
-
|
|
72
|
-
const key = `ad_view_${slot}`;
|
|
73
|
-
|
|
74
|
-
const obs = new IntersectionObserver(
|
|
75
|
-
(entries) => {
|
|
76
|
-
const entry = entries[0];
|
|
77
|
-
if (!entry?.isIntersecting) return;
|
|
78
|
-
|
|
79
|
-
const already = sessionStorage.getItem(key);
|
|
80
|
-
if (already) {
|
|
81
|
-
obs.unobserve(el);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
sessionStorage.setItem(key, "1");
|
|
86
|
-
send("view");
|
|
87
|
-
obs.unobserve(el);
|
|
88
|
-
},
|
|
89
|
-
{ threshold: 0.15 },
|
|
90
|
-
);
|
|
59
|
+
const adData = staticData ?? fetchedData;
|
|
91
60
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
61
|
+
const isEmpty =
|
|
62
|
+
!adData &&
|
|
63
|
+
!isLoading &&
|
|
64
|
+
(error instanceof Error
|
|
65
|
+
? error.message === AdDataQueryError.NO_AD
|
|
66
|
+
: !error);
|
|
95
67
|
|
|
96
68
|
const onClick = useCallback(
|
|
97
69
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
98
70
|
const target = e.target as HTMLElement;
|
|
99
|
-
const
|
|
71
|
+
const isInteractive =
|
|
100
72
|
target.tagName === "A" ||
|
|
101
73
|
target.tagName === "BUTTON" ||
|
|
102
74
|
target.closest("a") !== null ||
|
|
103
75
|
target.closest("button") !== null;
|
|
76
|
+
if (isInteractive) return;
|
|
104
77
|
|
|
105
|
-
if (
|
|
106
|
-
|
|
78
|
+
if (adData) {
|
|
79
|
+
performAdAction(adData);
|
|
80
|
+
} else if (isEmpty && slot) {
|
|
81
|
+
performEmptyAdAction(slot, chainId, baseLinkUrl);
|
|
107
82
|
}
|
|
108
83
|
},
|
|
109
|
-
[
|
|
84
|
+
[adData, isEmpty, slot, chainId, baseLinkUrl],
|
|
110
85
|
);
|
|
111
86
|
|
|
112
87
|
return (
|
|
113
|
-
<
|
|
114
|
-
{
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
{adData.type === "cast" && <CastAdContent data={adData} />}
|
|
129
|
-
{adData.type === "miniapp" && <MiniappAdContent data={adData} />}
|
|
130
|
-
{adData.type === "token" && <TokenAdContent data={adData} />}
|
|
131
|
-
{adData.type === "farcasterProfile" && (
|
|
132
|
-
<FarcasterProfileAdContent data={adData} />
|
|
133
|
-
)}
|
|
134
|
-
</>
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
return <EmtpyAdContent slot={slot} baseUrl={networkBaseUrl} />;
|
|
138
|
-
})()}
|
|
139
|
-
</div>
|
|
88
|
+
<AdContext.Provider
|
|
89
|
+
value={{
|
|
90
|
+
data: adData ?? null,
|
|
91
|
+
isLoading: !!slot && !staticData && isLoading,
|
|
92
|
+
error,
|
|
93
|
+
isEmpty,
|
|
94
|
+
slot,
|
|
95
|
+
baseLinkUrl,
|
|
96
|
+
chainId,
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
<div ref={ref} onClick={onClick} {...props}>
|
|
100
|
+
{children}
|
|
101
|
+
</div>
|
|
102
|
+
</AdContext.Provider>
|
|
140
103
|
);
|
|
141
104
|
}
|
|
142
105
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
<FileWarningIcon className="w-5 h-5 md:w-7 md:h-7" />
|
|
148
|
-
<p className="font-bold text-primary">ERROR</p>
|
|
149
|
-
</div>
|
|
150
|
-
</BasicAdBody>
|
|
151
|
-
);
|
|
106
|
+
// ─── Sub-components ──────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
export interface AdImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
|
109
|
+
fallback?: React.ReactNode;
|
|
152
110
|
}
|
|
153
111
|
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
112
|
+
export function AdImage({ fallback, ...props }: AdImageProps) {
|
|
113
|
+
const { data } = useAd();
|
|
114
|
+
const src = getAdImage(data);
|
|
115
|
+
if (!src) return fallback ? <>{fallback}</> : null;
|
|
116
|
+
return <img src={src} alt="" {...props} />;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface AdTitleProps extends React.HTMLAttributes<HTMLParagraphElement> {
|
|
120
|
+
fallback?: React.ReactNode;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function AdTitle({ fallback, children, ...props }: AdTitleProps) {
|
|
124
|
+
const { data } = useAd();
|
|
125
|
+
const title = getAdTitle(data);
|
|
126
|
+
if (!title) return fallback ? <>{fallback}</> : null;
|
|
127
|
+
return <p {...props}>{children ?? title}</p>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface AdDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {
|
|
131
|
+
fallback?: React.ReactNode;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function AdDescription({ fallback, children, ...props }: AdDescriptionProps) {
|
|
135
|
+
const { data } = useAd();
|
|
136
|
+
const description = getAdDescription(data);
|
|
137
|
+
if (!description) return fallback ? <>{fallback}</> : null;
|
|
138
|
+
return <p {...props}>{children ?? description}</p>;
|
|
163
139
|
}
|
|
164
140
|
|
|
165
|
-
|
|
141
|
+
export interface AdBadgeProps extends React.HTMLAttributes<HTMLSpanElement> {}
|
|
142
|
+
|
|
143
|
+
export function AdBadge({ children, ...props }: AdBadgeProps) {
|
|
144
|
+
const { data } = useAd();
|
|
145
|
+
const type = getAdType(data);
|
|
146
|
+
if (!type) return null;
|
|
147
|
+
const Icon = adCardIcon[type];
|
|
148
|
+
const label = adCardLabel[type];
|
|
166
149
|
return (
|
|
167
|
-
<
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
<SquareDashed className="w-5 h-5 md:w-7 md:h-7" />
|
|
176
|
-
<p className="font-bold text-primary">
|
|
177
|
-
NO AD{" "}
|
|
178
|
-
<span className="text-xs text-muted-foreground/80 font-normal">
|
|
179
|
-
(Your ad here)
|
|
180
|
-
</span>
|
|
181
|
-
</p>
|
|
182
|
-
</div>
|
|
183
|
-
</BasicAdBody>
|
|
150
|
+
<span {...props}>
|
|
151
|
+
{children ?? (
|
|
152
|
+
<>
|
|
153
|
+
{Icon && <Icon className="size-3" />}
|
|
154
|
+
{label}
|
|
155
|
+
</>
|
|
156
|
+
)}
|
|
157
|
+
</span>
|
|
184
158
|
);
|
|
185
159
|
}
|
|
160
|
+
|
|
161
|
+
export interface AdLabelProps extends React.HTMLAttributes<HTMLSpanElement> {}
|
|
162
|
+
|
|
163
|
+
export function AdLabel({ children, ...props }: AdLabelProps) {
|
|
164
|
+
return <span {...props}>{children ?? "AD"}</span>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─── State components ────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
export interface AdStatusProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
170
|
+
children?: React.ReactNode;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function AdLoading({ children, ...props }: AdStatusProps) {
|
|
174
|
+
const { isLoading } = useAd();
|
|
175
|
+
if (!isLoading) return null;
|
|
176
|
+
return <div {...props}>{children ?? "Loading..."}</div>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function AdEmpty({ children, ...props }: AdStatusProps) {
|
|
180
|
+
const { isEmpty } = useAd();
|
|
181
|
+
if (!isEmpty) return null;
|
|
182
|
+
return <div {...props}>{children ?? "Your ad here"}</div>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function AdError({ children, ...props }: AdStatusProps) {
|
|
186
|
+
const { error, isEmpty } = useAd();
|
|
187
|
+
if (!error || isEmpty) return null;
|
|
188
|
+
return <div {...props}>{children ?? "Error loading ad"}</div>;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function AdLoaded({ children, ...props }: AdStatusProps) {
|
|
192
|
+
const { data } = useAd();
|
|
193
|
+
if (!data) return null;
|
|
194
|
+
return <div {...props}>{children}</div>;
|
|
195
|
+
}
|
package/src/fetch.ts
CHANGED
|
@@ -1,7 +1,34 @@
|
|
|
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
6
|
|
|
3
7
|
const IPFS_GATEWAY = "https://gateway.pinata.cloud/ipfs/";
|
|
4
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
|
+
|
|
5
32
|
/**
|
|
6
33
|
* Fetch ad content from a metadata URI (IPFS or HTTP)
|
|
7
34
|
*/
|
|
@@ -29,52 +56,24 @@ export const fetchAdFromURI = async (uri: string) => {
|
|
|
29
56
|
};
|
|
30
57
|
|
|
31
58
|
/**
|
|
32
|
-
* Fetch the metadata URI for a slot
|
|
59
|
+
* Fetch the metadata URI for a slot using the SDK.
|
|
33
60
|
*/
|
|
34
61
|
export const fetchMetadataURI = async (
|
|
35
|
-
|
|
36
|
-
metadataModuleAddress: string,
|
|
62
|
+
client: SlotsClient,
|
|
37
63
|
slotAddress: string,
|
|
38
64
|
): Promise<string> => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const paddedSlot = slotAddress.slice(2).toLowerCase().padStart(64, "0");
|
|
42
|
-
// tokenURI(address) = 0xc87b56dd... no, it's a custom function
|
|
43
|
-
// bytes4(keccak256("tokenURI(address)"))
|
|
44
|
-
const selector = "0xe9dc6375"; // we'll compute it properly
|
|
45
|
-
|
|
46
|
-
const res = await fetch(rpcUrl, {
|
|
47
|
-
method: "POST",
|
|
48
|
-
headers: { "Content-Type": "application/json" },
|
|
49
|
-
body: JSON.stringify({
|
|
50
|
-
jsonrpc: "2.0",
|
|
51
|
-
method: "eth_call",
|
|
52
|
-
id: 1,
|
|
53
|
-
params: [
|
|
54
|
-
{
|
|
55
|
-
to: metadataModuleAddress,
|
|
56
|
-
data: `0x93702f33${paddedSlot}`, // tokenURI(address)
|
|
57
|
-
},
|
|
58
|
-
"latest",
|
|
59
|
-
],
|
|
60
|
-
}),
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
const json = await res.json();
|
|
64
|
-
if (!json.result || json.result === "0x") return "";
|
|
65
|
+
const info = await client.getSlotInfo(slotAddress as Address);
|
|
66
|
+
const moduleAddress = (info as { module: Address }).module;
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const length = parseInt(hex.slice(offset, offset + 64), 16);
|
|
71
|
-
const strHex = hex.slice(offset + 64, offset + 64 + length * 2);
|
|
72
|
-
let str = "";
|
|
73
|
-
for (let i = 0; i < strHex.length; i += 2) {
|
|
74
|
-
str += String.fromCharCode(parseInt(strHex.slice(i, i + 2), 16));
|
|
75
|
-
}
|
|
76
|
-
return str;
|
|
77
|
-
} catch {
|
|
68
|
+
if (
|
|
69
|
+
!moduleAddress ||
|
|
70
|
+
moduleAddress === "0x0000000000000000000000000000000000000000"
|
|
71
|
+
) {
|
|
78
72
|
return "";
|
|
79
73
|
}
|
|
74
|
+
|
|
75
|
+
return client.modules.metadata.getURI(
|
|
76
|
+
moduleAddress,
|
|
77
|
+
slotAddress as Address,
|
|
78
|
+
);
|
|
80
79
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SlotsChain } from "@0xslots/sdk";
|
|
2
|
+
import type { AdData } from "@adland/data";
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
|
|
5
|
+
export interface AdContextValue {
|
|
6
|
+
data: AdData | null;
|
|
7
|
+
isLoading: boolean;
|
|
8
|
+
error: unknown;
|
|
9
|
+
isEmpty: boolean;
|
|
10
|
+
slot?: string;
|
|
11
|
+
baseLinkUrl: string;
|
|
12
|
+
chainId: SlotsChain;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const AdContext = createContext<AdContextValue | null>(null);
|
|
16
|
+
|
|
17
|
+
export function useAd(): AdContextValue {
|
|
18
|
+
const ctx = useContext(AdContext);
|
|
19
|
+
if (!ctx) throw new Error("useAd must be used within an <Ad> component");
|
|
20
|
+
return ctx;
|
|
21
|
+
}
|
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,36 +1,37 @@
|
|
|
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 slot contract address (0xSlots v3)
|
|
6
|
+
* The slot contract address (0xSlots v3).
|
|
7
|
+
* Required when fetching from chain. Omit when passing static `data`.
|
|
8
8
|
*/
|
|
9
|
-
slot
|
|
9
|
+
slot?: string;
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* Static ad data. When provided, skips on-chain fetching.
|
|
12
12
|
*/
|
|
13
|
-
|
|
13
|
+
data?: AdData;
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* Chain ID for on-chain reads. Defaults to BASE (8453).
|
|
16
16
|
*/
|
|
17
|
-
|
|
17
|
+
chainId?: SlotsChain;
|
|
18
18
|
/**
|
|
19
|
-
* Optional
|
|
19
|
+
* Optional RPC URL override. If not provided, uses public RPC for the chain.
|
|
20
20
|
*/
|
|
21
|
-
|
|
21
|
+
rpcUrl?: string;
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
23
|
+
* Base URL for the "Your ad here" CTA link.
|
|
24
|
+
* Empty-state click navigates to `${baseLinkUrl}/slots/${slot}?chain=${chainId}`.
|
|
25
|
+
* Defaults to "https://app.0xslots.org".
|
|
24
26
|
*/
|
|
25
|
-
|
|
27
|
+
baseLinkUrl?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Compound children (AdImage, AdTitle, etc.)
|
|
30
|
+
*/
|
|
31
|
+
children?: React.ReactNode;
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
export enum AdDataQueryError {
|
|
29
35
|
NO_AD = "NO_AD",
|
|
30
36
|
ERROR = "ERROR",
|
|
31
37
|
}
|
|
32
|
-
|
|
33
|
-
export interface AdDataQueryResponse {
|
|
34
|
-
error?: AdDataQueryError;
|
|
35
|
-
data?: AdData;
|
|
36
|
-
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { SlotsChain } from "@0xslots/sdk";
|
|
2
|
+
import type { AdData } from "@adland/data";
|
|
3
|
+
import sdk from "@farcaster/miniapp-sdk";
|
|
4
|
+
|
|
5
|
+
async function isMiniApp(): Promise<boolean> {
|
|
6
|
+
try {
|
|
7
|
+
const context = await Promise.race([
|
|
8
|
+
sdk.context,
|
|
9
|
+
new Promise((r) => setTimeout(r, 500)),
|
|
10
|
+
]);
|
|
11
|
+
return !!context;
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function performAdAction(adData: AdData) {
|
|
18
|
+
try {
|
|
19
|
+
switch (adData.type) {
|
|
20
|
+
case "link":
|
|
21
|
+
sdk.actions.openUrl(adData.data.url);
|
|
22
|
+
break;
|
|
23
|
+
case "cast":
|
|
24
|
+
sdk.actions.viewCast({ hash: adData.data.hash });
|
|
25
|
+
break;
|
|
26
|
+
case "miniapp":
|
|
27
|
+
sdk.actions.openMiniApp({ url: adData.data.url });
|
|
28
|
+
break;
|
|
29
|
+
case "token":
|
|
30
|
+
sdk.actions.viewToken({ token: adData.data.address });
|
|
31
|
+
break;
|
|
32
|
+
case "farcasterProfile":
|
|
33
|
+
sdk.actions.viewProfile({
|
|
34
|
+
fid: Number.parseInt(adData.data.fid, 10),
|
|
35
|
+
});
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
// Fallback for web (non-miniapp) context
|
|
40
|
+
if (adData.type === "link" || adData.type === "miniapp") {
|
|
41
|
+
window.open(adData.data.url, "_blank");
|
|
42
|
+
} else {
|
|
43
|
+
console.error("[@adland/react] Failed to perform ad action:", err);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function performEmptyAdAction(
|
|
49
|
+
slot: string,
|
|
50
|
+
chainId: SlotsChain,
|
|
51
|
+
baseLinkUrl: string,
|
|
52
|
+
) {
|
|
53
|
+
const url = `${baseLinkUrl}/slots/${slot}?chain=${chainId}`;
|
|
54
|
+
if (await isMiniApp()) {
|
|
55
|
+
sdk.actions.openMiniApp({ url });
|
|
56
|
+
} else {
|
|
57
|
+
window.open(url, "_blank");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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,158 +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
|
-
slot: string;
|
|
37
|
-
},
|
|
38
|
-
): Promise<Response> {
|
|
39
|
-
if (!isSdkReady()) {
|
|
40
|
-
console.error(
|
|
41
|
-
"[@adland/react] SDK is not ready. Cannot send tracking request.",
|
|
42
|
-
);
|
|
43
|
-
return Promise.reject(
|
|
44
|
-
new Error(
|
|
45
|
-
"[@adland/react] SDK is not ready. Cannot send tracking request.",
|
|
46
|
-
),
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
if (!sdk.quickAuth) {
|
|
52
|
-
throw new Error("[@adland/react] SDK quickAuth is not available");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const requestOptions = {
|
|
56
|
-
method: "POST",
|
|
57
|
-
headers: {
|
|
58
|
-
"Content-Type": "application/json",
|
|
59
|
-
"x-auth-type": "farcaster",
|
|
60
|
-
},
|
|
61
|
-
body: JSON.stringify(data),
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
// Log request details for debugging
|
|
65
|
-
console.log("[@adland/react] Sending track request:", {
|
|
66
|
-
url,
|
|
67
|
-
method: requestOptions.method,
|
|
68
|
-
headers: requestOptions.headers,
|
|
69
|
-
body: data,
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
const response = await sdk.quickAuth.fetch(url, requestOptions);
|
|
73
|
-
|
|
74
|
-
// Check if response is ok
|
|
75
|
-
if (!response.ok) {
|
|
76
|
-
const statusText = response.statusText;
|
|
77
|
-
const status = response.status;
|
|
78
|
-
|
|
79
|
-
// Try to get error body if available
|
|
80
|
-
let errorBody = null;
|
|
81
|
-
try {
|
|
82
|
-
const contentType = response.headers.get("content-type");
|
|
83
|
-
if (contentType?.includes("application/json")) {
|
|
84
|
-
errorBody = await response.clone().json();
|
|
85
|
-
} else {
|
|
86
|
-
errorBody = await response.clone().text();
|
|
87
|
-
}
|
|
88
|
-
} catch (e) {
|
|
89
|
-
// Ignore errors when reading response body
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
console.error("[@adland/react] Track request failed:", {
|
|
93
|
-
url,
|
|
94
|
-
status,
|
|
95
|
-
statusText,
|
|
96
|
-
errorBody,
|
|
97
|
-
requestBody: data,
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
throw new Error(
|
|
101
|
-
`[@adland/react] Track request failed: ${status} ${statusText}${errorBody ? ` - ${JSON.stringify(errorBody)}` : ""}`,
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return response;
|
|
106
|
-
} catch (error) {
|
|
107
|
-
// Enhanced error logging
|
|
108
|
-
if (error instanceof Error) {
|
|
109
|
-
console.error("[@adland/react] Error sending track request:", {
|
|
110
|
-
message: error.message,
|
|
111
|
-
name: error.name,
|
|
112
|
-
stack: error.stack,
|
|
113
|
-
url,
|
|
114
|
-
requestData: data,
|
|
115
|
-
});
|
|
116
|
-
} else {
|
|
117
|
-
console.error("[@adland/react] Unknown error sending track request:", {
|
|
118
|
-
error,
|
|
119
|
-
url,
|
|
120
|
-
requestData: data,
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
throw error;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Checks if SDK actions are ready
|
|
129
|
-
*/
|
|
130
|
-
export async function checkSdkActionsReady(): Promise<boolean> {
|
|
131
|
-
try {
|
|
132
|
-
if (!sdk) {
|
|
133
|
-
console.error("[@adland/react] SDK is not available");
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (!sdk.actions) {
|
|
138
|
-
console.error("[@adland/react] SDK actions are not available");
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Check if ready() function exists
|
|
143
|
-
if (typeof sdk.actions.ready !== "function") {
|
|
144
|
-
console.error(
|
|
145
|
-
"[@adland/react] SDK actions.ready() is not available. Make sure the SDK is properly initialized.",
|
|
146
|
-
);
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return true;
|
|
151
|
-
} catch (error) {
|
|
152
|
-
console.error(
|
|
153
|
-
"[@adland/react] Error checking SDK actions readiness:",
|
|
154
|
-
error,
|
|
155
|
-
);
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
}
|