@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 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.13.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
- "@adland/data": "0.14.0"
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",
@@ -1,185 +1,195 @@
1
- import { useEffect, useRef, useCallback } from "react";
2
- import { sendTrackRequest } from "../utils/sdk";
3
- import sdk from "@farcaster/miniapp-sdk";
4
- import { FileWarningIcon, Loader, SquareDashed } from "lucide-react";
5
- import { type AdData } from "@adland/data";
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 BasicAdBody from "./BasicAdBody";
8
- import LinkAdContent from "./content/LinkAdContent";
9
- import MiniappAdContent from "./content/MiniappAdContent";
10
- import CastAdContent from "./content/CastAdContent";
11
- import TokenAdContent from "./content/TokenAdContent";
12
- import FarcasterProfileAdContent from "./content/FarcasterProfileAdContent";
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 powered by 0xSlots v3 MetadataModule.
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
- metadataModule = DEFAULT_METADATA_MODULE,
32
- network = "testnet",
33
- baseUrl,
34
- rpcUrl = DEFAULT_RPC,
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
- const networkBaseUrl = baseUrl ?? getBaseUrl(network);
38
+
39
+ const client = useMemo(
40
+ () => (slot ? createReadClient(chainId, rpcUrl) : null),
41
+ [slot, chainId, rpcUrl],
42
+ );
39
43
 
40
44
  const {
41
- data: adData,
45
+ data: fetchedData,
42
46
  isLoading,
43
47
  error,
44
48
  } = useFetch<AdData>(
45
49
  `ad-data-${slot}`,
46
50
  async () => {
47
- const uri = await fetchMetadataURI(rpcUrl, metadataModule, slot);
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 send = useCallback(
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
- obs.observe(el);
93
- return () => obs.disconnect();
94
- }, [slot, send]);
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 isInteractiveElement =
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 (!isInteractiveElement) {
106
- send("click");
78
+ if (adData) {
79
+ performAdAction(adData);
80
+ } else if (isEmpty && slot) {
81
+ performEmptyAdAction(slot, chainId, baseLinkUrl);
107
82
  }
108
83
  },
109
- [send],
84
+ [adData, isEmpty, slot, chainId, baseLinkUrl],
110
85
  );
111
86
 
112
87
  return (
113
- <div ref={ref} onClick={onClick} {...props}>
114
- {(() => {
115
- if (isLoading) return <LoadingAdContent />;
116
- if (error) {
117
- if (error instanceof Error) {
118
- if (error.message === AdDataQueryError.NO_AD) {
119
- return <EmtpyAdContent slot={slot} baseUrl={networkBaseUrl} />;
120
- }
121
- }
122
- return <ErrorAdContent error={error} />;
123
- }
124
- if (adData) {
125
- return (
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>
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
- function ErrorAdContent({ error }: { error: unknown }) {
144
- return (
145
- <BasicAdBody>
146
- <div className="flex flex-row items-center gap-2">
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 LoadingAdContent() {
155
- return (
156
- <BasicAdBody>
157
- <div className="flex flex-row items-center gap-2">
158
- <Loader className="w-5 h-5 md:w-7 md:h-7 animate-spin" />
159
- <p className="font-bold text-primary">LOADING...</p>
160
- </div>
161
- </BasicAdBody>
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
- function EmtpyAdContent({ slot, baseUrl }: { slot: string; baseUrl: string }) {
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
- <BasicAdBody
168
- onClick={() => {
169
- sdk.actions.openMiniApp({
170
- url: `${baseUrl}/slots/${slot}`,
171
- });
172
- }}
173
- >
174
- <div className="flex flex-row items-center gap-2">
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 from the MetadataModule contract via RPC
59
+ * Fetch the metadata URI for a slot using the SDK.
33
60
  */
34
61
  export const fetchMetadataURI = async (
35
- rpcUrl: string,
36
- metadataModuleAddress: string,
62
+ client: SlotsClient,
37
63
  slotAddress: string,
38
64
  ): Promise<string> => {
39
- // tokenURI(address) selector = 0xc2bc2efc (keccak of tokenURI(address))
40
- // Actually, tokenURI takes address param, need proper encoding
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
- // Decode ABI-encoded string
67
- try {
68
- const hex = json.result.slice(2);
69
- const offset = parseInt(hex.slice(0, 64), 16) * 2;
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
- // Main exports
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
- isSdkReady,
8
- sendTrackRequest,
9
- checkSdkActionsReady,
10
- } from "./utils/sdk";
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 exports
13
- export * from "./utils/constants";
34
+ // Constants
35
+ export { adCardIcon, adCardLabel } from "./utils/constants";
package/src/types.ts CHANGED
@@ -1,36 +1,37 @@
1
- import { AdData } from "@adland/data";
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: string;
9
+ slot?: string;
10
10
  /**
11
- * MetadataModule contract address (to fetch ad content URI)
11
+ * Static ad data. When provided, skips on-chain fetching.
12
12
  */
13
- metadataModule?: string;
13
+ data?: AdData;
14
14
  /**
15
- * Network to use for tracking requests (currently only "testnet" is supported)
15
+ * Chain ID for on-chain reads. Defaults to BASE (8453).
16
16
  */
17
- network?: Network;
17
+ chainId?: SlotsChain;
18
18
  /**
19
- * Optional base URL override. If not provided, uses relative URL in browser or production URL in SSR
19
+ * Optional RPC URL override. If not provided, uses public RPC for the chain.
20
20
  */
21
- baseUrl?: string;
21
+ rpcUrl?: string;
22
22
  /**
23
- * RPC URL for reading MetadataModule on-chain
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
- rpcUrl?: string;
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;
@@ -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
- }