@adland/react 0.12.1 → 0.13.1

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