@adland/react 0.13.0 → 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 +9 -0
- package/package.json +4 -2
- package/src/components/Ad.tsx +139 -136
- package/src/fetch.ts +40 -41
- package/src/hooks/useAdContext.ts +18 -0
- package/src/index.ts +33 -11
- package/src/types.ts +13 -18
- package/src/utils/ad-actions.ts +26 -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,14 @@
|
|
|
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
|
+
|
|
3
12
|
## 0.13.0
|
|
4
13
|
|
|
5
14
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adland/react",
|
|
3
|
-
"version": "0.13.
|
|
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,185 +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 { 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 } 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
|
+
children,
|
|
35
34
|
...props
|
|
36
35
|
}: AdProps) {
|
|
37
36
|
const ref = useRef<HTMLDivElement>(null);
|
|
38
|
-
|
|
37
|
+
|
|
38
|
+
const client = useMemo(
|
|
39
|
+
() => (slot ? createReadClient(chainId, rpcUrl) : null),
|
|
40
|
+
[slot, chainId, rpcUrl],
|
|
41
|
+
);
|
|
39
42
|
|
|
40
43
|
const {
|
|
41
|
-
data:
|
|
44
|
+
data: fetchedData,
|
|
42
45
|
isLoading,
|
|
43
46
|
error,
|
|
44
47
|
} = useFetch<AdData>(
|
|
45
48
|
`ad-data-${slot}`,
|
|
46
49
|
async () => {
|
|
47
|
-
|
|
50
|
+
if (!client || !slot) throw new Error(AdDataQueryError.NO_AD);
|
|
51
|
+
const uri = await fetchMetadataURI(client, slot);
|
|
48
52
|
if (!uri) throw new Error(AdDataQueryError.NO_AD);
|
|
49
53
|
return fetchAdFromURI(uri);
|
|
50
54
|
},
|
|
51
|
-
{ enabled: !!slot },
|
|
55
|
+
{ enabled: !!slot && !staticData },
|
|
52
56
|
);
|
|
53
57
|
|
|
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
|
-
);
|
|
58
|
+
const adData = staticData ?? fetchedData;
|
|
91
59
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
60
|
+
const isEmpty =
|
|
61
|
+
!adData &&
|
|
62
|
+
!isLoading &&
|
|
63
|
+
(error instanceof Error
|
|
64
|
+
? error.message === AdDataQueryError.NO_AD
|
|
65
|
+
: !error);
|
|
95
66
|
|
|
96
67
|
const onClick = useCallback(
|
|
97
68
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
98
69
|
const target = e.target as HTMLElement;
|
|
99
|
-
const
|
|
70
|
+
const isInteractive =
|
|
100
71
|
target.tagName === "A" ||
|
|
101
72
|
target.tagName === "BUTTON" ||
|
|
102
73
|
target.closest("a") !== null ||
|
|
103
74
|
target.closest("button") !== null;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
send("click");
|
|
75
|
+
if (!isInteractive && adData) {
|
|
76
|
+
performAdAction(adData);
|
|
107
77
|
}
|
|
108
78
|
},
|
|
109
|
-
[
|
|
79
|
+
[adData],
|
|
110
80
|
);
|
|
111
81
|
|
|
112
82
|
return (
|
|
113
|
-
<
|
|
114
|
-
{
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<>
|
|
127
|
-
{adData.type === "link" && <LinkAdContent data={adData} />}
|
|
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>
|
|
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>
|
|
140
96
|
);
|
|
141
97
|
}
|
|
142
98
|
|
|
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
|
-
);
|
|
99
|
+
// ─── Sub-components ──────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
export interface AdImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
|
102
|
+
fallback?: React.ReactNode;
|
|
152
103
|
}
|
|
153
104
|
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
<p className="font-bold text-primary">LOADING...</p>
|
|
160
|
-
</div>
|
|
161
|
-
</BasicAdBody>
|
|
162
|
-
);
|
|
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} />;
|
|
163
110
|
}
|
|
164
111
|
|
|
165
|
-
|
|
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;
|
|
125
|
+
}
|
|
126
|
+
|
|
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];
|
|
166
142
|
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>
|
|
143
|
+
<span {...props}>
|
|
144
|
+
{children ?? (
|
|
145
|
+
<>
|
|
146
|
+
{Icon && <Icon className="size-3" />}
|
|
147
|
+
{label}
|
|
148
|
+
</>
|
|
149
|
+
)}
|
|
150
|
+
</span>
|
|
184
151
|
);
|
|
185
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,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,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,36 +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 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
|
+
* Compound children (AdImage, AdTitle, etc.)
|
|
24
24
|
*/
|
|
25
|
-
|
|
25
|
+
children?: React.ReactNode;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export enum AdDataQueryError {
|
|
29
29
|
NO_AD = "NO_AD",
|
|
30
30
|
ERROR = "ERROR",
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
export interface AdDataQueryResponse {
|
|
34
|
-
error?: AdDataQueryError;
|
|
35
|
-
data?: AdData;
|
|
36
|
-
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
-
}
|