@medialane/ui 0.107.1 → 0.109.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.
Files changed (49) hide show
  1. package/dist/components/listing-card.cjs +4 -2
  2. package/dist/components/listing-card.cjs.map +1 -1
  3. package/dist/components/listing-card.d.cts +8 -1
  4. package/dist/components/listing-card.d.ts +8 -1
  5. package/dist/components/listing-card.js +4 -2
  6. package/dist/components/listing-card.js.map +1 -1
  7. package/dist/components/remixes-tab.cjs +91 -0
  8. package/dist/components/remixes-tab.cjs.map +1 -0
  9. package/dist/components/remixes-tab.d.cts +11 -0
  10. package/dist/components/remixes-tab.d.ts +11 -0
  11. package/dist/components/remixes-tab.js +57 -0
  12. package/dist/components/remixes-tab.js.map +1 -0
  13. package/dist/data/notification.cjs +17 -0
  14. package/dist/data/notification.cjs.map +1 -0
  15. package/dist/data/notification.d.cts +34 -0
  16. package/dist/data/notification.d.ts +34 -0
  17. package/dist/data/notification.js +1 -0
  18. package/dist/data/notification.js.map +1 -0
  19. package/dist/index.cjs +31 -2
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +6 -0
  22. package/dist/index.d.ts +6 -0
  23. package/dist/index.js +26 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/utils/api-fetch.cjs +65 -0
  26. package/dist/utils/api-fetch.cjs.map +1 -0
  27. package/dist/utils/api-fetch.d.cts +38 -0
  28. package/dist/utils/api-fetch.d.ts +38 -0
  29. package/dist/utils/api-fetch.js +40 -0
  30. package/dist/utils/api-fetch.js.map +1 -0
  31. package/dist/utils/use-notifications.cjs +169 -0
  32. package/dist/utils/use-notifications.cjs.map +1 -0
  33. package/dist/utils/use-notifications.d.cts +12 -0
  34. package/dist/utils/use-notifications.d.ts +12 -0
  35. package/dist/utils/use-notifications.js +140 -0
  36. package/dist/utils/use-notifications.js.map +1 -0
  37. package/dist/utils/use-orders.cjs +153 -0
  38. package/dist/utils/use-orders.cjs.map +1 -0
  39. package/dist/utils/use-orders.d.cts +55 -0
  40. package/dist/utils/use-orders.d.ts +55 -0
  41. package/dist/utils/use-orders.js +113 -0
  42. package/dist/utils/use-orders.js.map +1 -0
  43. package/dist/utils/use-remix-offers.cjs +49 -0
  44. package/dist/utils/use-remix-offers.cjs.map +1 -0
  45. package/dist/utils/use-remix-offers.d.cts +19 -0
  46. package/dist/utils/use-remix-offers.d.ts +19 -0
  47. package/dist/utils/use-remix-offers.js +15 -0
  48. package/dist/utils/use-remix-offers.js.map +1 -0
  49. package/package.json +1 -1
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Thin fetch helper for backend `/v1/*` routes that are not yet exposed via
3
+ * `@medialane/sdk`'s `ApiClient`. Takes `{ baseUrl, apiKey }` as an explicit
4
+ * config rather than reading env vars directly — each app's own
5
+ * `MEDIALANE_BACKEND_URL` resolution is environment-aware (server gets the
6
+ * real backend URL + key; the browser gets a same-origin BFF proxy that
7
+ * injects the real key server-side) and that routing/security logic stays
8
+ * app-local, same as `getMedialaneClient`.
9
+ *
10
+ * Error model:
11
+ * `ApiError` carries the HTTP `status` so callers can special-case auth
12
+ * states (401/403) without a generic error toast.
13
+ *
14
+ * Headers:
15
+ * - `x-api-key` is injected when `apiKey` is non-empty.
16
+ * - `Authorization: Bearer <token>` is forwarded if the caller passes
17
+ * `bearer` — used for SIWS-gated routes.
18
+ * - `Content-Type: application/json` is set automatically when a body is
19
+ * present.
20
+ */
21
+ declare class ApiError extends Error {
22
+ readonly status: number;
23
+ constructor(status: number, message: string);
24
+ }
25
+ interface ApiFetchConfig {
26
+ baseUrl: string;
27
+ apiKey?: string;
28
+ }
29
+ interface ApiFetchOptions {
30
+ method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
31
+ body?: unknown;
32
+ /** Bearer token (SIWS) for identity-aware routes. */
33
+ bearer?: string | null;
34
+ signal?: AbortSignal;
35
+ }
36
+ declare function apiFetch<T = unknown>(config: ApiFetchConfig, path: string, options?: ApiFetchOptions): Promise<T>;
37
+
38
+ export { ApiError, type ApiFetchConfig, type ApiFetchOptions, apiFetch };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Thin fetch helper for backend `/v1/*` routes that are not yet exposed via
3
+ * `@medialane/sdk`'s `ApiClient`. Takes `{ baseUrl, apiKey }` as an explicit
4
+ * config rather than reading env vars directly — each app's own
5
+ * `MEDIALANE_BACKEND_URL` resolution is environment-aware (server gets the
6
+ * real backend URL + key; the browser gets a same-origin BFF proxy that
7
+ * injects the real key server-side) and that routing/security logic stays
8
+ * app-local, same as `getMedialaneClient`.
9
+ *
10
+ * Error model:
11
+ * `ApiError` carries the HTTP `status` so callers can special-case auth
12
+ * states (401/403) without a generic error toast.
13
+ *
14
+ * Headers:
15
+ * - `x-api-key` is injected when `apiKey` is non-empty.
16
+ * - `Authorization: Bearer <token>` is forwarded if the caller passes
17
+ * `bearer` — used for SIWS-gated routes.
18
+ * - `Content-Type: application/json` is set automatically when a body is
19
+ * present.
20
+ */
21
+ declare class ApiError extends Error {
22
+ readonly status: number;
23
+ constructor(status: number, message: string);
24
+ }
25
+ interface ApiFetchConfig {
26
+ baseUrl: string;
27
+ apiKey?: string;
28
+ }
29
+ interface ApiFetchOptions {
30
+ method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
31
+ body?: unknown;
32
+ /** Bearer token (SIWS) for identity-aware routes. */
33
+ bearer?: string | null;
34
+ signal?: AbortSignal;
35
+ }
36
+ declare function apiFetch<T = unknown>(config: ApiFetchConfig, path: string, options?: ApiFetchOptions): Promise<T>;
37
+
38
+ export { ApiError, type ApiFetchConfig, type ApiFetchOptions, apiFetch };
@@ -0,0 +1,40 @@
1
+ class ApiError extends Error {
2
+ constructor(status, message) {
3
+ super(message);
4
+ this.status = status;
5
+ this.name = "ApiError";
6
+ }
7
+ }
8
+ async function apiFetch(config, path, options = {}) {
9
+ const { method = "GET", body, bearer, signal } = options;
10
+ const url = `${config.baseUrl.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
11
+ const headers = {};
12
+ if (body !== void 0) headers["Content-Type"] = "application/json";
13
+ if (config.apiKey) headers["x-api-key"] = config.apiKey;
14
+ if (bearer) headers["Authorization"] = `Bearer ${bearer}`;
15
+ const res = await fetch(url, {
16
+ method,
17
+ headers,
18
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
19
+ signal
20
+ });
21
+ if (!res.ok) {
22
+ const text = await res.text().catch(() => res.statusText);
23
+ let message = text || `Request failed with HTTP ${res.status}`;
24
+ try {
25
+ const parsed = JSON.parse(text);
26
+ if (parsed && typeof parsed === "object" && "error" in parsed && typeof parsed.error === "string") {
27
+ message = parsed.error;
28
+ }
29
+ } catch {
30
+ }
31
+ throw new ApiError(res.status, message);
32
+ }
33
+ if (res.status === 204) return void 0;
34
+ return res.json();
35
+ }
36
+ export {
37
+ ApiError,
38
+ apiFetch
39
+ };
40
+ //# sourceMappingURL=api-fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/api-fetch.ts"],"sourcesContent":["/**\n * Thin fetch helper for backend `/v1/*` routes that are not yet exposed via\n * `@medialane/sdk`'s `ApiClient`. Takes `{ baseUrl, apiKey }` as an explicit\n * config rather than reading env vars directly — each app's own\n * `MEDIALANE_BACKEND_URL` resolution is environment-aware (server gets the\n * real backend URL + key; the browser gets a same-origin BFF proxy that\n * injects the real key server-side) and that routing/security logic stays\n * app-local, same as `getMedialaneClient`.\n *\n * Error model:\n * `ApiError` carries the HTTP `status` so callers can special-case auth\n * states (401/403) without a generic error toast.\n *\n * Headers:\n * - `x-api-key` is injected when `apiKey` is non-empty.\n * - `Authorization: Bearer <token>` is forwarded if the caller passes\n * `bearer` — used for SIWS-gated routes.\n * - `Content-Type: application/json` is set automatically when a body is\n * present.\n */\nexport class ApiError extends Error {\n constructor(public readonly status: number, message: string) {\n super(message);\n this.name = \"ApiError\";\n }\n}\n\nexport interface ApiFetchConfig {\n baseUrl: string;\n apiKey?: string;\n}\n\nexport interface ApiFetchOptions {\n method?: \"GET\" | \"POST\" | \"PATCH\" | \"PUT\" | \"DELETE\";\n body?: unknown;\n /** Bearer token (SIWS) for identity-aware routes. */\n bearer?: string | null;\n signal?: AbortSignal;\n}\n\nexport async function apiFetch<T = unknown>(\n config: ApiFetchConfig,\n path: string,\n options: ApiFetchOptions = {}\n): Promise<T> {\n const { method = \"GET\", body, bearer, signal } = options;\n const url = `${config.baseUrl.replace(/\\/$/, \"\")}${path.startsWith(\"/\") ? path : `/${path}`}`;\n\n const headers: Record<string, string> = {};\n if (body !== undefined) headers[\"Content-Type\"] = \"application/json\";\n if (config.apiKey) headers[\"x-api-key\"] = config.apiKey;\n if (bearer) headers[\"Authorization\"] = `Bearer ${bearer}`;\n\n const res = await fetch(url, {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n signal,\n });\n\n if (!res.ok) {\n const text = await res.text().catch(() => res.statusText);\n let message = text || `Request failed with HTTP ${res.status}`;\n try {\n const parsed = JSON.parse(text);\n if (parsed && typeof parsed === \"object\" && \"error\" in parsed && typeof parsed.error === \"string\") {\n message = parsed.error;\n }\n } catch {\n /* response wasn't JSON — keep the text as the message */\n }\n throw new ApiError(res.status, message);\n }\n\n // 204 No Content shows up on some PATCH/DELETE paths — return undefined as T.\n if (res.status === 204) return undefined as T;\n return res.json() as Promise<T>;\n}\n"],"mappings":"AAoBO,MAAM,iBAAiB,MAAM;AAAA,EAClC,YAA4B,QAAgB,SAAiB;AAC3D,UAAM,OAAO;AADa;AAE1B,SAAK,OAAO;AAAA,EACd;AACF;AAeA,eAAsB,SACpB,QACA,MACA,UAA2B,CAAC,GAChB;AACZ,QAAM,EAAE,SAAS,OAAO,MAAM,QAAQ,OAAO,IAAI;AACjD,QAAM,MAAM,GAAG,OAAO,QAAQ,QAAQ,OAAO,EAAE,CAAC,GAAG,KAAK,WAAW,GAAG,IAAI,OAAO,IAAI,IAAI,EAAE;AAE3F,QAAM,UAAkC,CAAC;AACzC,MAAI,SAAS,OAAW,SAAQ,cAAc,IAAI;AAClD,MAAI,OAAO,OAAQ,SAAQ,WAAW,IAAI,OAAO;AACjD,MAAI,OAAQ,SAAQ,eAAe,IAAI,UAAU,MAAM;AAEvD,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B;AAAA,IACA;AAAA,IACA,MAAM,SAAS,SAAY,KAAK,UAAU,IAAI,IAAI;AAAA,IAClD;AAAA,EACF,CAAC;AAED,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI,UAAU;AACxD,QAAI,UAAU,QAAQ,4BAA4B,IAAI,MAAM;AAC5D,QAAI;AACF,YAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,UAAI,UAAU,OAAO,WAAW,YAAY,WAAW,UAAU,OAAO,OAAO,UAAU,UAAU;AACjG,kBAAU,OAAO;AAAA,MACnB;AAAA,IACF,QAAQ;AAAA,IAER;AACA,UAAM,IAAI,SAAS,IAAI,QAAQ,OAAO;AAAA,EACxC;AAGA,MAAI,IAAI,WAAW,IAAK,QAAO;AAC/B,SAAO,IAAI,KAAK;AAClB;","names":[]}
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ "use client";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+ var use_notifications_exports = {};
31
+ __export(use_notifications_exports, {
32
+ useNotifications: () => useNotifications
33
+ });
34
+ module.exports = __toCommonJS(use_notifications_exports);
35
+ var import_react = require("react");
36
+ var import_swr = __toESM(require("swr"), 1);
37
+ var import_sdk = require("@medialane/sdk");
38
+ var import_use_orders = require("./use-orders.js");
39
+ var import_use_activities = require("./use-activities.js");
40
+ var import_notification_storage = require("./notification-storage.js");
41
+ var import_format_activity = require("./format-activity.js");
42
+ async function fetchAnnouncements() {
43
+ const res = await fetch("/api/announcements");
44
+ if (!res.ok) return [];
45
+ return res.json();
46
+ }
47
+ function useNotifications(getClient, apiConfig, address) {
48
+ const [readIds, setReadIds] = (0, import_react.useState)(() => (0, import_notification_storage.getReadIds)());
49
+ const { orders: userOrders } = (0, import_use_orders.useUserOrders)(getClient, address ?? null);
50
+ const { orders: receivedOffers } = (0, import_use_orders.useReceivedOffers)(apiConfig, address ?? null);
51
+ const { activities } = (0, import_use_activities.useActivitiesByAddress)(getClient, address ?? null);
52
+ const { data: announcements = [] } = (0, import_swr.default)(
53
+ "announcements",
54
+ fetchAnnouncements,
55
+ { revalidateOnFocus: false, dedupingInterval: 6e4 }
56
+ );
57
+ const notifications = (0, import_react.useMemo)(() => {
58
+ const items = [];
59
+ userOrders.filter((o) => o.offer.itemType === "ERC20" && o.status === "FULFILLED").forEach((order) => {
60
+ const id = `offer-accepted-${order.orderHash}`;
61
+ const fmt = (0, import_format_activity.formatOfferAcceptedNotification)(order);
62
+ items.push({
63
+ id,
64
+ type: "offer_accepted",
65
+ priority: "spotlight",
66
+ celebratory: true,
67
+ ...fmt,
68
+ timestamp: order.updatedAt ?? order.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
69
+ isUnread: !readIds.has(id),
70
+ metadata: {
71
+ amount: order.price?.formatted ?? void 0,
72
+ currency: order.price?.currency ?? void 0,
73
+ txHash: order.txHash?.fulfilled ?? void 0,
74
+ assetName: order.token?.name ?? void 0
75
+ }
76
+ });
77
+ });
78
+ receivedOffers.forEach((order) => {
79
+ const id = `order-${order.orderHash}`;
80
+ const fmt = (0, import_format_activity.formatOrderNotification)(order);
81
+ items.push({
82
+ id,
83
+ type: "offer",
84
+ priority: "spotlight",
85
+ celebratory: false,
86
+ ...fmt,
87
+ timestamp: order.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
88
+ isUnread: !readIds.has(id),
89
+ metadata: {
90
+ amount: order.price?.formatted ?? void 0,
91
+ currency: order.price?.currency ?? void 0,
92
+ assetName: order.token?.name ?? void 0
93
+ }
94
+ });
95
+ });
96
+ activities.slice(0, 50).forEach((event) => {
97
+ const id = `activity-${event.txHash}-${event.type}-${event.nftTokenId ?? ""}`;
98
+ if (event.type === "transfer" && address && !!event.to && (0, import_sdk.normalizeAddress)("STARKNET", event.to) === (0, import_sdk.normalizeAddress)("STARKNET", address)) {
99
+ const fmt2 = (0, import_format_activity.formatAssetReceivedNotification)(event);
100
+ items.push({
101
+ id,
102
+ type: "asset_received",
103
+ priority: "spotlight",
104
+ celebratory: true,
105
+ ...fmt2,
106
+ timestamp: event.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
107
+ isUnread: !readIds.has(id),
108
+ metadata: {
109
+ txHash: event.txHash ?? void 0,
110
+ assetName: event.token?.name ?? void 0
111
+ }
112
+ });
113
+ return;
114
+ }
115
+ const isMySale = event.type === "sale" && address && (!!event.offerer && (0, import_sdk.normalizeAddress)("STARKNET", event.offerer) === (0, import_sdk.normalizeAddress)("STARKNET", address) || !!event.from && (0, import_sdk.normalizeAddress)("STARKNET", event.from) === (0, import_sdk.normalizeAddress)("STARKNET", address));
116
+ const fmt = (0, import_format_activity.formatActivity)(event);
117
+ items.push({
118
+ id,
119
+ type: event.type,
120
+ priority: isMySale ? "spotlight" : "normal",
121
+ celebratory: !!isMySale,
122
+ ...fmt,
123
+ timestamp: event.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
124
+ isUnread: !readIds.has(id),
125
+ metadata: {
126
+ amount: event.price?.formatted ?? void 0,
127
+ currency: event.price?.currency ?? void 0,
128
+ txHash: event.txHash ?? void 0,
129
+ assetName: event.token?.name ?? void 0
130
+ }
131
+ });
132
+ });
133
+ announcements.forEach((ann) => {
134
+ const id = `ann-${ann.id}`;
135
+ items.push({
136
+ id,
137
+ type: "announcement",
138
+ priority: ann.pinned ? "spotlight" : "normal",
139
+ celebratory: false,
140
+ title: ann.title,
141
+ description: ann.body,
142
+ image: ann.image,
143
+ href: ann.href,
144
+ timestamp: ann.created_at,
145
+ isUnread: !readIds.has(id)
146
+ });
147
+ });
148
+ items.sort(
149
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
150
+ );
151
+ return items;
152
+ }, [userOrders, receivedOffers, activities, announcements, address, readIds]);
153
+ const unreadCount = notifications.filter((n) => n.isUnread).length;
154
+ const markNotificationsRead = (0, import_react.useCallback)((ids) => {
155
+ (0, import_notification_storage.markRead)(ids);
156
+ setReadIds((0, import_notification_storage.getReadIds)());
157
+ }, []);
158
+ return {
159
+ notifications,
160
+ unreadCount,
161
+ markAllRead: () => markNotificationsRead(notifications.map((n) => n.id)),
162
+ markRead: (id) => markNotificationsRead([id])
163
+ };
164
+ }
165
+ // Annotate the CommonJS export names for ESM import in node:
166
+ 0 && (module.exports = {
167
+ useNotifications
168
+ });
169
+ //# sourceMappingURL=use-notifications.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/use-notifications.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useMemo, useState } from \"react\";\nimport useSWR from \"swr\";\nimport { normalizeAddress } from \"@medialane/sdk\";\nimport type { ApiActivity } from \"@medialane/sdk\";\nimport type { MedialaneClient } from \"@medialane/sdk/starknet\";\nimport { useUserOrders, useReceivedOffers } from \"./use-orders.js\";\nimport { useActivitiesByAddress } from \"./use-activities.js\";\nimport { getReadIds, markRead } from \"./notification-storage.js\";\nimport {\n formatActivity,\n formatOrderNotification,\n formatOfferAcceptedNotification,\n formatAssetReceivedNotification,\n} from \"./format-activity.js\";\nimport type { Notification, Announcement } from \"../data/notification.js\";\nimport type { ApiFetchConfig } from \"./api-fetch.js\";\n\nasync function fetchAnnouncements(): Promise<Announcement[]> {\n const res = await fetch(\"/api/announcements\");\n if (!res.ok) return [];\n return res.json();\n}\n\nexport function useNotifications(\n getClient: () => MedialaneClient,\n apiConfig: ApiFetchConfig,\n address: string | null | undefined\n) {\n const [readIds, setReadIds] = useState<Set<string>>(() => getReadIds());\n\n // Offers this user placed (to detect accepted ones)\n const { orders: userOrders } = useUserOrders(getClient, address ?? null);\n // Offers received on tokens the user holds\n const { orders: receivedOffers } = useReceivedOffers(apiConfig, address ?? null);\n const { activities } = useActivitiesByAddress(getClient, address ?? null);\n const { data: announcements = [] } = useSWR<Announcement[]>(\n \"announcements\",\n fetchAnnouncements,\n { revalidateOnFocus: false, dedupingInterval: 60_000 }\n );\n\n const notifications: Notification[] = useMemo(() => {\n const items: Notification[] = [];\n\n // ── Offers accepted (buyer: my ERC20 bid was fulfilled) ──────────────────\n userOrders\n .filter((o) => o.offer.itemType === \"ERC20\" && o.status === \"FULFILLED\")\n .forEach((order) => {\n const id = `offer-accepted-${order.orderHash}`;\n const fmt = formatOfferAcceptedNotification(order);\n items.push({\n id,\n type: \"offer_accepted\",\n priority: \"spotlight\",\n celebratory: true,\n ...fmt,\n timestamp: order.updatedAt ?? order.createdAt ?? new Date().toISOString(),\n isUnread: !readIds.has(id),\n metadata: {\n amount: order.price?.formatted ?? undefined,\n currency: order.price?.currency ?? undefined,\n txHash: order.txHash?.fulfilled ?? undefined,\n assetName: order.token?.name ?? undefined,\n },\n });\n });\n\n // ── Received offers (someone bid on my asset) ────────────────────────────\n receivedOffers.forEach((order) => {\n const id = `order-${order.orderHash}`;\n const fmt = formatOrderNotification(order);\n items.push({\n id,\n type: \"offer\",\n priority: \"spotlight\",\n celebratory: false,\n ...fmt,\n timestamp: order.createdAt ?? new Date().toISOString(),\n isUnread: !readIds.has(id),\n metadata: {\n amount: order.price?.formatted ?? undefined,\n currency: order.price?.currency ?? undefined,\n assetName: order.token?.name ?? undefined,\n },\n });\n });\n\n // ── Activity events ──────────────────────────────────────────────────────\n (activities as ApiActivity[]).slice(0, 50).forEach((event) => {\n const id = `activity-${event.txHash}-${event.type}-${event.nftTokenId ?? \"\"}`;\n\n // Asset received: transfer where this user is the recipient\n if (\n event.type === \"transfer\" &&\n address &&\n !!event.to && normalizeAddress(\"STARKNET\", event.to) === normalizeAddress(\"STARKNET\", address)\n ) {\n const fmt = formatAssetReceivedNotification(event);\n items.push({\n id,\n type: \"asset_received\",\n priority: \"spotlight\",\n celebratory: true,\n ...fmt,\n timestamp: event.timestamp ?? new Date().toISOString(),\n isUnread: !readIds.has(id),\n metadata: {\n txHash: event.txHash ?? undefined,\n assetName: event.token?.name ?? undefined,\n },\n });\n return;\n }\n\n // Sale (seller perspective): activity sale where offerer = address\n const isMySale =\n event.type === \"sale\" &&\n address &&\n ((!!event.offerer && normalizeAddress(\"STARKNET\", event.offerer) === normalizeAddress(\"STARKNET\", address)) ||\n (!!event.from && normalizeAddress(\"STARKNET\", event.from) === normalizeAddress(\"STARKNET\", address)));\n\n const fmt = formatActivity(event);\n items.push({\n id,\n type: event.type as Notification[\"type\"],\n priority: isMySale ? \"spotlight\" : \"normal\",\n celebratory: !!isMySale,\n ...fmt,\n timestamp: event.timestamp ?? new Date().toISOString(),\n isUnread: !readIds.has(id),\n metadata: {\n amount: event.price?.formatted ?? undefined,\n currency: event.price?.currency ?? undefined,\n txHash: event.txHash ?? undefined,\n assetName: event.token?.name ?? undefined,\n },\n });\n });\n\n // ── Announcements ────────────────────────────────────────────────────────\n announcements.forEach((ann) => {\n const id = `ann-${ann.id}`;\n items.push({\n id,\n type: \"announcement\",\n priority: ann.pinned ? \"spotlight\" : \"normal\",\n celebratory: false,\n title: ann.title,\n description: ann.body,\n image: ann.image,\n href: ann.href,\n timestamp: ann.created_at,\n isUnread: !readIds.has(id),\n });\n });\n\n // Chronological, newest first\n items.sort(\n (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()\n );\n\n return items;\n }, [userOrders, receivedOffers, activities, announcements, address, readIds]);\n\n const unreadCount = notifications.filter((n) => n.isUnread).length;\n\n const markNotificationsRead = useCallback((ids: string[]) => {\n markRead(ids);\n setReadIds(getReadIds());\n }, []);\n\n return {\n notifications,\n unreadCount,\n markAllRead: () => markNotificationsRead(notifications.map((n) => n.id)),\n markRead: (id: string) => markNotificationsRead([id]),\n };\n}\n"],"mappings":"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","names":["useSWR","fmt"]}
@@ -0,0 +1,12 @@
1
+ import { MedialaneClient } from '@medialane/sdk/starknet';
2
+ import { Notification } from '../data/notification.cjs';
3
+ import { ApiFetchConfig } from './api-fetch.cjs';
4
+
5
+ declare function useNotifications(getClient: () => MedialaneClient, apiConfig: ApiFetchConfig, address: string | null | undefined): {
6
+ notifications: Notification[];
7
+ unreadCount: number;
8
+ markAllRead: () => void;
9
+ markRead: (id: string) => void;
10
+ };
11
+
12
+ export { useNotifications };
@@ -0,0 +1,12 @@
1
+ import { MedialaneClient } from '@medialane/sdk/starknet';
2
+ import { Notification } from '../data/notification.js';
3
+ import { ApiFetchConfig } from './api-fetch.js';
4
+
5
+ declare function useNotifications(getClient: () => MedialaneClient, apiConfig: ApiFetchConfig, address: string | null | undefined): {
6
+ notifications: Notification[];
7
+ unreadCount: number;
8
+ markAllRead: () => void;
9
+ markRead: (id: string) => void;
10
+ };
11
+
12
+ export { useNotifications };
@@ -0,0 +1,140 @@
1
+ "use client";
2
+ import { useCallback, useMemo, useState } from "react";
3
+ import useSWR from "swr";
4
+ import { normalizeAddress } from "@medialane/sdk";
5
+ import { useUserOrders, useReceivedOffers } from "./use-orders.js";
6
+ import { useActivitiesByAddress } from "./use-activities.js";
7
+ import { getReadIds, markRead } from "./notification-storage.js";
8
+ import {
9
+ formatActivity,
10
+ formatOrderNotification,
11
+ formatOfferAcceptedNotification,
12
+ formatAssetReceivedNotification
13
+ } from "./format-activity.js";
14
+ async function fetchAnnouncements() {
15
+ const res = await fetch("/api/announcements");
16
+ if (!res.ok) return [];
17
+ return res.json();
18
+ }
19
+ function useNotifications(getClient, apiConfig, address) {
20
+ const [readIds, setReadIds] = useState(() => getReadIds());
21
+ const { orders: userOrders } = useUserOrders(getClient, address ?? null);
22
+ const { orders: receivedOffers } = useReceivedOffers(apiConfig, address ?? null);
23
+ const { activities } = useActivitiesByAddress(getClient, address ?? null);
24
+ const { data: announcements = [] } = useSWR(
25
+ "announcements",
26
+ fetchAnnouncements,
27
+ { revalidateOnFocus: false, dedupingInterval: 6e4 }
28
+ );
29
+ const notifications = useMemo(() => {
30
+ const items = [];
31
+ userOrders.filter((o) => o.offer.itemType === "ERC20" && o.status === "FULFILLED").forEach((order) => {
32
+ const id = `offer-accepted-${order.orderHash}`;
33
+ const fmt = formatOfferAcceptedNotification(order);
34
+ items.push({
35
+ id,
36
+ type: "offer_accepted",
37
+ priority: "spotlight",
38
+ celebratory: true,
39
+ ...fmt,
40
+ timestamp: order.updatedAt ?? order.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
41
+ isUnread: !readIds.has(id),
42
+ metadata: {
43
+ amount: order.price?.formatted ?? void 0,
44
+ currency: order.price?.currency ?? void 0,
45
+ txHash: order.txHash?.fulfilled ?? void 0,
46
+ assetName: order.token?.name ?? void 0
47
+ }
48
+ });
49
+ });
50
+ receivedOffers.forEach((order) => {
51
+ const id = `order-${order.orderHash}`;
52
+ const fmt = formatOrderNotification(order);
53
+ items.push({
54
+ id,
55
+ type: "offer",
56
+ priority: "spotlight",
57
+ celebratory: false,
58
+ ...fmt,
59
+ timestamp: order.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
60
+ isUnread: !readIds.has(id),
61
+ metadata: {
62
+ amount: order.price?.formatted ?? void 0,
63
+ currency: order.price?.currency ?? void 0,
64
+ assetName: order.token?.name ?? void 0
65
+ }
66
+ });
67
+ });
68
+ activities.slice(0, 50).forEach((event) => {
69
+ const id = `activity-${event.txHash}-${event.type}-${event.nftTokenId ?? ""}`;
70
+ if (event.type === "transfer" && address && !!event.to && normalizeAddress("STARKNET", event.to) === normalizeAddress("STARKNET", address)) {
71
+ const fmt2 = formatAssetReceivedNotification(event);
72
+ items.push({
73
+ id,
74
+ type: "asset_received",
75
+ priority: "spotlight",
76
+ celebratory: true,
77
+ ...fmt2,
78
+ timestamp: event.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
79
+ isUnread: !readIds.has(id),
80
+ metadata: {
81
+ txHash: event.txHash ?? void 0,
82
+ assetName: event.token?.name ?? void 0
83
+ }
84
+ });
85
+ return;
86
+ }
87
+ const isMySale = event.type === "sale" && address && (!!event.offerer && normalizeAddress("STARKNET", event.offerer) === normalizeAddress("STARKNET", address) || !!event.from && normalizeAddress("STARKNET", event.from) === normalizeAddress("STARKNET", address));
88
+ const fmt = formatActivity(event);
89
+ items.push({
90
+ id,
91
+ type: event.type,
92
+ priority: isMySale ? "spotlight" : "normal",
93
+ celebratory: !!isMySale,
94
+ ...fmt,
95
+ timestamp: event.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
96
+ isUnread: !readIds.has(id),
97
+ metadata: {
98
+ amount: event.price?.formatted ?? void 0,
99
+ currency: event.price?.currency ?? void 0,
100
+ txHash: event.txHash ?? void 0,
101
+ assetName: event.token?.name ?? void 0
102
+ }
103
+ });
104
+ });
105
+ announcements.forEach((ann) => {
106
+ const id = `ann-${ann.id}`;
107
+ items.push({
108
+ id,
109
+ type: "announcement",
110
+ priority: ann.pinned ? "spotlight" : "normal",
111
+ celebratory: false,
112
+ title: ann.title,
113
+ description: ann.body,
114
+ image: ann.image,
115
+ href: ann.href,
116
+ timestamp: ann.created_at,
117
+ isUnread: !readIds.has(id)
118
+ });
119
+ });
120
+ items.sort(
121
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
122
+ );
123
+ return items;
124
+ }, [userOrders, receivedOffers, activities, announcements, address, readIds]);
125
+ const unreadCount = notifications.filter((n) => n.isUnread).length;
126
+ const markNotificationsRead = useCallback((ids) => {
127
+ markRead(ids);
128
+ setReadIds(getReadIds());
129
+ }, []);
130
+ return {
131
+ notifications,
132
+ unreadCount,
133
+ markAllRead: () => markNotificationsRead(notifications.map((n) => n.id)),
134
+ markRead: (id) => markNotificationsRead([id])
135
+ };
136
+ }
137
+ export {
138
+ useNotifications
139
+ };
140
+ //# sourceMappingURL=use-notifications.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/use-notifications.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useMemo, useState } from \"react\";\nimport useSWR from \"swr\";\nimport { normalizeAddress } from \"@medialane/sdk\";\nimport type { ApiActivity } from \"@medialane/sdk\";\nimport type { MedialaneClient } from \"@medialane/sdk/starknet\";\nimport { useUserOrders, useReceivedOffers } from \"./use-orders.js\";\nimport { useActivitiesByAddress } from \"./use-activities.js\";\nimport { getReadIds, markRead } from \"./notification-storage.js\";\nimport {\n formatActivity,\n formatOrderNotification,\n formatOfferAcceptedNotification,\n formatAssetReceivedNotification,\n} from \"./format-activity.js\";\nimport type { Notification, Announcement } from \"../data/notification.js\";\nimport type { ApiFetchConfig } from \"./api-fetch.js\";\n\nasync function fetchAnnouncements(): Promise<Announcement[]> {\n const res = await fetch(\"/api/announcements\");\n if (!res.ok) return [];\n return res.json();\n}\n\nexport function useNotifications(\n getClient: () => MedialaneClient,\n apiConfig: ApiFetchConfig,\n address: string | null | undefined\n) {\n const [readIds, setReadIds] = useState<Set<string>>(() => getReadIds());\n\n // Offers this user placed (to detect accepted ones)\n const { orders: userOrders } = useUserOrders(getClient, address ?? null);\n // Offers received on tokens the user holds\n const { orders: receivedOffers } = useReceivedOffers(apiConfig, address ?? null);\n const { activities } = useActivitiesByAddress(getClient, address ?? null);\n const { data: announcements = [] } = useSWR<Announcement[]>(\n \"announcements\",\n fetchAnnouncements,\n { revalidateOnFocus: false, dedupingInterval: 60_000 }\n );\n\n const notifications: Notification[] = useMemo(() => {\n const items: Notification[] = [];\n\n // ── Offers accepted (buyer: my ERC20 bid was fulfilled) ──────────────────\n userOrders\n .filter((o) => o.offer.itemType === \"ERC20\" && o.status === \"FULFILLED\")\n .forEach((order) => {\n const id = `offer-accepted-${order.orderHash}`;\n const fmt = formatOfferAcceptedNotification(order);\n items.push({\n id,\n type: \"offer_accepted\",\n priority: \"spotlight\",\n celebratory: true,\n ...fmt,\n timestamp: order.updatedAt ?? order.createdAt ?? new Date().toISOString(),\n isUnread: !readIds.has(id),\n metadata: {\n amount: order.price?.formatted ?? undefined,\n currency: order.price?.currency ?? undefined,\n txHash: order.txHash?.fulfilled ?? undefined,\n assetName: order.token?.name ?? undefined,\n },\n });\n });\n\n // ── Received offers (someone bid on my asset) ────────────────────────────\n receivedOffers.forEach((order) => {\n const id = `order-${order.orderHash}`;\n const fmt = formatOrderNotification(order);\n items.push({\n id,\n type: \"offer\",\n priority: \"spotlight\",\n celebratory: false,\n ...fmt,\n timestamp: order.createdAt ?? new Date().toISOString(),\n isUnread: !readIds.has(id),\n metadata: {\n amount: order.price?.formatted ?? undefined,\n currency: order.price?.currency ?? undefined,\n assetName: order.token?.name ?? undefined,\n },\n });\n });\n\n // ── Activity events ──────────────────────────────────────────────────────\n (activities as ApiActivity[]).slice(0, 50).forEach((event) => {\n const id = `activity-${event.txHash}-${event.type}-${event.nftTokenId ?? \"\"}`;\n\n // Asset received: transfer where this user is the recipient\n if (\n event.type === \"transfer\" &&\n address &&\n !!event.to && normalizeAddress(\"STARKNET\", event.to) === normalizeAddress(\"STARKNET\", address)\n ) {\n const fmt = formatAssetReceivedNotification(event);\n items.push({\n id,\n type: \"asset_received\",\n priority: \"spotlight\",\n celebratory: true,\n ...fmt,\n timestamp: event.timestamp ?? new Date().toISOString(),\n isUnread: !readIds.has(id),\n metadata: {\n txHash: event.txHash ?? undefined,\n assetName: event.token?.name ?? undefined,\n },\n });\n return;\n }\n\n // Sale (seller perspective): activity sale where offerer = address\n const isMySale =\n event.type === \"sale\" &&\n address &&\n ((!!event.offerer && normalizeAddress(\"STARKNET\", event.offerer) === normalizeAddress(\"STARKNET\", address)) ||\n (!!event.from && normalizeAddress(\"STARKNET\", event.from) === normalizeAddress(\"STARKNET\", address)));\n\n const fmt = formatActivity(event);\n items.push({\n id,\n type: event.type as Notification[\"type\"],\n priority: isMySale ? \"spotlight\" : \"normal\",\n celebratory: !!isMySale,\n ...fmt,\n timestamp: event.timestamp ?? new Date().toISOString(),\n isUnread: !readIds.has(id),\n metadata: {\n amount: event.price?.formatted ?? undefined,\n currency: event.price?.currency ?? undefined,\n txHash: event.txHash ?? undefined,\n assetName: event.token?.name ?? undefined,\n },\n });\n });\n\n // ── Announcements ────────────────────────────────────────────────────────\n announcements.forEach((ann) => {\n const id = `ann-${ann.id}`;\n items.push({\n id,\n type: \"announcement\",\n priority: ann.pinned ? \"spotlight\" : \"normal\",\n celebratory: false,\n title: ann.title,\n description: ann.body,\n image: ann.image,\n href: ann.href,\n timestamp: ann.created_at,\n isUnread: !readIds.has(id),\n });\n });\n\n // Chronological, newest first\n items.sort(\n (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()\n );\n\n return items;\n }, [userOrders, receivedOffers, activities, announcements, address, readIds]);\n\n const unreadCount = notifications.filter((n) => n.isUnread).length;\n\n const markNotificationsRead = useCallback((ids: string[]) => {\n markRead(ids);\n setReadIds(getReadIds());\n }, []);\n\n return {\n notifications,\n unreadCount,\n markAllRead: () => markNotificationsRead(notifications.map((n) => n.id)),\n markRead: (id: string) => markNotificationsRead([id]),\n };\n}\n"],"mappings":";AAEA,SAAS,aAAa,SAAS,gBAAgB;AAC/C,OAAO,YAAY;AACnB,SAAS,wBAAwB;AAGjC,SAAS,eAAe,yBAAyB;AACjD,SAAS,8BAA8B;AACvC,SAAS,YAAY,gBAAgB;AACrC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAIP,eAAe,qBAA8C;AAC3D,QAAM,MAAM,MAAM,MAAM,oBAAoB;AAC5C,MAAI,CAAC,IAAI,GAAI,QAAO,CAAC;AACrB,SAAO,IAAI,KAAK;AAClB;AAEO,SAAS,iBACd,WACA,WACA,SACA;AACA,QAAM,CAAC,SAAS,UAAU,IAAI,SAAsB,MAAM,WAAW,CAAC;AAGtE,QAAM,EAAE,QAAQ,WAAW,IAAI,cAAc,WAAW,WAAW,IAAI;AAEvE,QAAM,EAAE,QAAQ,eAAe,IAAI,kBAAkB,WAAW,WAAW,IAAI;AAC/E,QAAM,EAAE,WAAW,IAAI,uBAAuB,WAAW,WAAW,IAAI;AACxE,QAAM,EAAE,MAAM,gBAAgB,CAAC,EAAE,IAAI;AAAA,IACnC;AAAA,IACA;AAAA,IACA,EAAE,mBAAmB,OAAO,kBAAkB,IAAO;AAAA,EACvD;AAEA,QAAM,gBAAgC,QAAQ,MAAM;AAClD,UAAM,QAAwB,CAAC;AAG/B,eACG,OAAO,CAAC,MAAM,EAAE,MAAM,aAAa,WAAW,EAAE,WAAW,WAAW,EACtE,QAAQ,CAAC,UAAU;AAClB,YAAM,KAAK,kBAAkB,MAAM,SAAS;AAC5C,YAAM,MAAM,gCAAgC,KAAK;AACjD,YAAM,KAAK;AAAA,QACT;AAAA,QACA,MAAM;AAAA,QACN,UAAU;AAAA,QACV,aAAa;AAAA,QACb,GAAG;AAAA,QACH,WAAW,MAAM,aAAa,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,QACxE,UAAU,CAAC,QAAQ,IAAI,EAAE;AAAA,QACzB,UAAU;AAAA,UACR,QAAQ,MAAM,OAAO,aAAa;AAAA,UAClC,UAAU,MAAM,OAAO,YAAY;AAAA,UACnC,QAAQ,MAAM,QAAQ,aAAa;AAAA,UACnC,WAAW,MAAM,OAAO,QAAQ;AAAA,QAClC;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAGH,mBAAe,QAAQ,CAAC,UAAU;AAChC,YAAM,KAAK,SAAS,MAAM,SAAS;AACnC,YAAM,MAAM,wBAAwB,KAAK;AACzC,YAAM,KAAK;AAAA,QACT;AAAA,QACA,MAAM;AAAA,QACN,UAAU;AAAA,QACV,aAAa;AAAA,QACb,GAAG;AAAA,QACH,WAAW,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,QACrD,UAAU,CAAC,QAAQ,IAAI,EAAE;AAAA,QACzB,UAAU;AAAA,UACR,QAAQ,MAAM,OAAO,aAAa;AAAA,UAClC,UAAU,MAAM,OAAO,YAAY;AAAA,UACnC,WAAW,MAAM,OAAO,QAAQ;AAAA,QAClC;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAGD,IAAC,WAA6B,MAAM,GAAG,EAAE,EAAE,QAAQ,CAAC,UAAU;AAC5D,YAAM,KAAK,YAAY,MAAM,MAAM,IAAI,MAAM,IAAI,IAAI,MAAM,cAAc,EAAE;AAG3E,UACE,MAAM,SAAS,cACf,WACA,CAAC,CAAC,MAAM,MAAM,iBAAiB,YAAY,MAAM,EAAE,MAAM,iBAAiB,YAAY,OAAO,GAC7F;AACA,cAAMA,OAAM,gCAAgC,KAAK;AACjD,cAAM,KAAK;AAAA,UACT;AAAA,UACA,MAAM;AAAA,UACN,UAAU;AAAA,UACV,aAAa;AAAA,UACb,GAAGA;AAAA,UACH,WAAW,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,UACrD,UAAU,CAAC,QAAQ,IAAI,EAAE;AAAA,UACzB,UAAU;AAAA,YACR,QAAQ,MAAM,UAAU;AAAA,YACxB,WAAW,MAAM,OAAO,QAAQ;AAAA,UAClC;AAAA,QACF,CAAC;AACD;AAAA,MACF;AAGA,YAAM,WACJ,MAAM,SAAS,UACf,YACE,CAAC,CAAC,MAAM,WAAW,iBAAiB,YAAY,MAAM,OAAO,MAAM,iBAAiB,YAAY,OAAO,KACtG,CAAC,CAAC,MAAM,QAAQ,iBAAiB,YAAY,MAAM,IAAI,MAAM,iBAAiB,YAAY,OAAO;AAEtG,YAAM,MAAM,eAAe,KAAK;AAChC,YAAM,KAAK;AAAA,QACT;AAAA,QACA,MAAM,MAAM;AAAA,QACZ,UAAU,WAAW,cAAc;AAAA,QACnC,aAAa,CAAC,CAAC;AAAA,QACf,GAAG;AAAA,QACH,WAAW,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,QACrD,UAAU,CAAC,QAAQ,IAAI,EAAE;AAAA,QACzB,UAAU;AAAA,UACR,QAAQ,MAAM,OAAO,aAAa;AAAA,UAClC,UAAU,MAAM,OAAO,YAAY;AAAA,UACnC,QAAQ,MAAM,UAAU;AAAA,UACxB,WAAW,MAAM,OAAO,QAAQ;AAAA,QAClC;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAGD,kBAAc,QAAQ,CAAC,QAAQ;AAC7B,YAAM,KAAK,OAAO,IAAI,EAAE;AACxB,YAAM,KAAK;AAAA,QACT;AAAA,QACA,MAAM;AAAA,QACN,UAAU,IAAI,SAAS,cAAc;AAAA,QACrC,aAAa;AAAA,QACb,OAAO,IAAI;AAAA,QACX,aAAa,IAAI;AAAA,QACjB,OAAO,IAAI;AAAA,QACX,MAAM,IAAI;AAAA,QACV,WAAW,IAAI;AAAA,QACf,UAAU,CAAC,QAAQ,IAAI,EAAE;AAAA,MAC3B,CAAC;AAAA,IACH,CAAC;AAGD,UAAM;AAAA,MACJ,CAAC,GAAG,MAAM,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,IAAI,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ;AAAA,IAC5E;AAEA,WAAO;AAAA,EACT,GAAG,CAAC,YAAY,gBAAgB,YAAY,eAAe,SAAS,OAAO,CAAC;AAE5E,QAAM,cAAc,cAAc,OAAO,CAAC,MAAM,EAAE,QAAQ,EAAE;AAE5D,QAAM,wBAAwB,YAAY,CAAC,QAAkB;AAC3D,aAAS,GAAG;AACZ,eAAW,WAAW,CAAC;AAAA,EACzB,GAAG,CAAC,CAAC;AAEL,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,aAAa,MAAM,sBAAsB,cAAc,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AAAA,IACvE,UAAU,CAAC,OAAe,sBAAsB,CAAC,EAAE,CAAC;AAAA,EACtD;AACF;","names":["fmt"]}