@timardex/cluemart-shared 2.5.0 → 2.5.2

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 (47) hide show
  1. package/dist/{ad-DRI2CIzV.d.ts → ad-C7GbDyUi.d.ts} +1 -1
  2. package/dist/{ad-BSyVGv8q.d.mts → ad-DB9gvcJW.d.mts} +1 -1
  3. package/dist/{chunk-DEK7NF3O.mjs → chunk-T736WX7J.mjs} +5 -5
  4. package/dist/{chunk-DEK7NF3O.mjs.map → chunk-T736WX7J.mjs.map} +1 -1
  5. package/dist/{chunk-4CQLKIQ6.mjs → chunk-TGEYOSSX.mjs} +24 -2
  6. package/dist/chunk-TGEYOSSX.mjs.map +1 -0
  7. package/dist/{chunk-2RHUGVV3.mjs → chunk-WOBG6I4T.mjs} +175 -12
  8. package/dist/chunk-WOBG6I4T.mjs.map +1 -0
  9. package/dist/{chunk-L4NHGGT7.mjs → chunk-WRYRBSRZ.mjs} +2 -2
  10. package/dist/chunk-YCUKTVVA.mjs +69 -0
  11. package/dist/chunk-YCUKTVVA.mjs.map +1 -0
  12. package/dist/{chunk-CQ26N3W6.mjs → chunk-YJJUTHBX.mjs} +2 -2
  13. package/dist/formFields/index.cjs.map +1 -1
  14. package/dist/formFields/index.mjs +4 -4
  15. package/dist/graphql/index.cjs +193 -13
  16. package/dist/graphql/index.cjs.map +1 -1
  17. package/dist/graphql/index.d.mts +46 -3
  18. package/dist/graphql/index.d.ts +46 -3
  19. package/dist/graphql/index.mjs +8 -2
  20. package/dist/hooks/index.cjs +23 -13
  21. package/dist/hooks/index.cjs.map +1 -1
  22. package/dist/hooks/index.d.mts +1 -1
  23. package/dist/hooks/index.d.ts +1 -1
  24. package/dist/hooks/index.mjs +6 -6
  25. package/dist/index.cjs +236 -16
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.mts +77 -2
  28. package/dist/index.d.ts +77 -2
  29. package/dist/index.mjs +221 -12
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/sharing/index.cjs +2 -1
  32. package/dist/sharing/index.cjs.map +1 -1
  33. package/dist/sharing/index.mjs +2 -2
  34. package/dist/types/index.d.mts +1 -1
  35. package/dist/types/index.d.ts +1 -1
  36. package/dist/utils/index.cjs +61 -3
  37. package/dist/utils/index.cjs.map +1 -1
  38. package/dist/utils/index.d.mts +33 -1
  39. package/dist/utils/index.d.ts +33 -1
  40. package/dist/utils/index.mjs +20 -4
  41. package/package.json +1 -1
  42. package/dist/chunk-2RHUGVV3.mjs.map +0 -1
  43. package/dist/chunk-4CQLKIQ6.mjs.map +0 -1
  44. package/dist/chunk-7DQBUZXY.mjs +0 -41
  45. package/dist/chunk-7DQBUZXY.mjs.map +0 -1
  46. /package/dist/{chunk-L4NHGGT7.mjs.map → chunk-WRYRBSRZ.mjs.map} +0 -0
  47. /package/dist/{chunk-CQ26N3W6.mjs.map → chunk-YJJUTHBX.mjs.map} +0 -0
@@ -161,10 +161,11 @@ function nzDayKey(input) {
161
161
  function isUtcMidnight(value) {
162
162
  return value instanceof Date && !Number.isNaN(value.getTime()) && value.getUTCHours() === 0 && value.getUTCMinutes() === 0 && value.getUTCSeconds() === 0 && value.getUTCMilliseconds() === 0;
163
163
  }
164
+ var DATE_DISPLAY_FORMAT = "dddd, D MMMM, YYYY";
164
165
  var formatDate = (dateStr, display = "datetime", timeStr) => {
165
166
  const dateTimeStr = timeStr ? `${dateStr} ${timeStr}` : dateStr;
166
167
  const dateTime = timeStr ? dayjs2(dateTimeStr, `${dateFormat} ${timeFormat}`) : dayjs2(dateStr, dateFormat);
167
- const formattedDate = dateTime.format("dddd, D MMMM, YYYY");
168
+ const formattedDate = dateTime.format(DATE_DISPLAY_FORMAT);
168
169
  const formattedTime = dateTime.format("h:mm a");
169
170
  switch (display) {
170
171
  case "date":
@@ -177,6 +178,25 @@ var formatDate = (dateStr, display = "datetime", timeStr) => {
177
178
  return formattedDate;
178
179
  }
179
180
  };
181
+ function parseStrictDate(dateStr) {
182
+ return dayjs2(dateStr, dateFormat, true);
183
+ }
184
+ function isDayBeforeNzToday(parsed) {
185
+ const dayKey = /* @__PURE__ */ new Date(`${parsed.format(isoDateFormat)}T00:00:00Z`);
186
+ return dayKey.getTime() < nzDayKey().getTime();
187
+ }
188
+ function isPastDate(dateStr) {
189
+ const parsed = parseStrictDate(dateStr);
190
+ return parsed.isValid() && isDayBeforeNzToday(parsed);
191
+ }
192
+ function formatDateOptionLabel(dateStr) {
193
+ const parsed = parseStrictDate(dateStr);
194
+ if (!parsed.isValid()) {
195
+ return dateStr;
196
+ }
197
+ const label = parsed.format(DATE_DISPLAY_FORMAT);
198
+ return isDayBeforeNzToday(parsed) ? `${label} (Past)` : label;
199
+ }
180
200
  var SLOT_FORMATS = [
181
201
  `${dateFormat} ${timeFormat}`,
182
202
  `${dateFormat} H:mm`,
@@ -413,6 +433,8 @@ export {
413
433
  nzDayKey,
414
434
  isUtcMidnight,
415
435
  formatDate,
436
+ isPastDate,
437
+ formatDateOptionLabel,
416
438
  getCurrentAndFutureDates,
417
439
  isFutureStartDate,
418
440
  isFutureDatesBeforeThreshold,
@@ -464,4 +486,4 @@ export {
464
486
  buildShareOgDescription,
465
487
  stripOverlaySubtitleSpecialChars
466
488
  };
467
- //# sourceMappingURL=chunk-4CQLKIQ6.mjs.map
489
+ //# sourceMappingURL=chunk-TGEYOSSX.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/dateFormats.ts","../src/utils/eventDateStatus.ts","../src/utils/date.ts","../src/sharing/relationShareTypes.ts","../src/sharing/constants.ts","../src/sharing/joinShareDescriptionSections.ts","../src/sharing/normalizeShareDescription.ts"],"sourcesContent":["export const dateFormat = \"DD-MM-YYYY\";\nexport const timeFormat = \"HH:mm\";\n/** ISO 8601 machine/sortable format: day keys, Date construction, strict parsing, HTML date inputs. */\nexport const isoDateFormat = \"YYYY-MM-DD\";\n","import dayjs from \"dayjs\";\nimport customParseFormat from \"dayjs/plugin/customParseFormat.js\";\nimport isoWeek from \"dayjs/plugin/isoWeek.js\";\n\nimport { EnumEventDateStatus } from \"../enums\";\nimport type { DateTimeType } from \"../types/global\";\n\nimport { dateFormat, timeFormat } from \"./dateFormats\";\n\n// Enable dayjs plugins used by event date parsing/status logic\ndayjs.extend(customParseFormat);\ndayjs.extend(isoWeek);\n\n/**\n * Display label per date status. Single source of truth instead of deriving\n * the label with `replaceAll(\"_\", \" \")` / text-transform tricks, so each\n * status maps to an explicit, correctly-cased string (e.g. \"This Week\",\n * \"Starting Soon\", \"Rescheduled\"). Lives here (not in `calendar`) so both\n * `calendar/eventCalendar` and `utils/date` can consume it without a circular\n * import.\n */\nexport const EVENT_DATE_STATUS_LABELS: Record<EnumEventDateStatus, string> = {\n [EnumEventDateStatus.STARTING_SOON]: \"Starting Soon\",\n [EnumEventDateStatus.STARTED]: \"Started\",\n [EnumEventDateStatus.TODAY]: \"Today\",\n [EnumEventDateStatus.TOMORROW]: \"Tomorrow\",\n [EnumEventDateStatus.THIS_WEEK]: \"This Week\",\n [EnumEventDateStatus.NEXT_WEEK]: \"Next Week\",\n [EnumEventDateStatus.UPCOMING]: \"Upcoming\",\n [EnumEventDateStatus.ENDED]: \"Ended\",\n [EnumEventDateStatus.RE_SCHEDULED]: \"Rescheduled\",\n [EnumEventDateStatus.CANCELED]: \"Canceled\",\n [EnumEventDateStatus.INVALID]: \"Invalid\",\n};\n\nexport const EVENT_DATE_STATUS_PERIOD_MAP: Partial<\n Record<EnumEventDateStatus, EnumEventDateStatus[]>\n> = {\n [EnumEventDateStatus.TODAY]: [\n EnumEventDateStatus.STARTED,\n EnumEventDateStatus.STARTING_SOON,\n EnumEventDateStatus.TODAY,\n ],\n [EnumEventDateStatus.THIS_WEEK]: [\n EnumEventDateStatus.STARTED,\n EnumEventDateStatus.STARTING_SOON,\n EnumEventDateStatus.THIS_WEEK,\n EnumEventDateStatus.TODAY,\n EnumEventDateStatus.TOMORROW,\n ],\n};\n\nexport function getAllowedEventDateStatuses(\n period: EnumEventDateStatus,\n): EnumEventDateStatus[] {\n return EVENT_DATE_STATUS_PERIOD_MAP[period] ?? [period];\n}\n\nexport function eventMatchesDateStatusPeriod(\n dateTime: Pick<DateTimeType, \"dateStatus\">[] | null | undefined,\n period: EnumEventDateStatus,\n): boolean {\n const allowedStatuses = getAllowedEventDateStatuses(period);\n\n return (\n dateTime?.some((dt) => allowedStatuses.includes(dt.dateStatus)) ?? false\n );\n}\n\nexport function filterEventsByDateStatusPeriod<\n T extends { dateTime?: Pick<DateTimeType, \"dateStatus\">[] | null },\n>(events: T[], period: EnumEventDateStatus): T[] {\n return events.filter((event) =>\n eventMatchesDateStatusPeriod(event.dateTime, period),\n );\n}\n\n/**\n * Canonical list of date statuses whose start date/time is still in the future\n * (i.e. the slot has not started yet). `STARTED`, `ENDED`, `INVALID`,\n * `CANCELED` and `RE_SCHEDULED` are intentionally excluded.\n *\n * This is the single source of truth for \"is this slot still in the future\".\n * `futureTimePeriods` (in `utils/date.ts`) derives from this list for UI\n * filters, additionally excluding `STARTING_SOON`.\n */\nexport const FUTURE_EVENT_DATE_STATUSES: readonly EnumEventDateStatus[] = [\n EnumEventDateStatus.STARTING_SOON,\n EnumEventDateStatus.TODAY,\n EnumEventDateStatus.TOMORROW,\n EnumEventDateStatus.THIS_WEEK,\n EnumEventDateStatus.NEXT_WEEK,\n EnumEventDateStatus.UPCOMING,\n];\n\nexport function isFutureEventDateStatus(\n dateStatus: EnumEventDateStatus | null | undefined,\n): boolean {\n return Boolean(dateStatus && FUTURE_EVENT_DATE_STATUSES.includes(dateStatus));\n}\n\nexport function filterDatesByFutureStatus<\n T extends { dateStatus?: EnumEventDateStatus | null },\n>(dates: T[]): T[] {\n return dates.filter((date) => isFutureEventDateStatus(date.dateStatus));\n}\n\n/**\n * Statuses set by an explicit user action rather than derived from the\n * schedule clock. The status recomputation below must leave them untouched,\n * otherwise a canceled/rescheduled date would silently flip back to a\n * time-derived status on the next run.\n */\nexport const MANUAL_DATE_STATUSES: ReadonlySet<EnumEventDateStatus> = new Set([\n EnumEventDateStatus.CANCELED,\n EnumEventDateStatus.RE_SCHEDULED,\n]);\n\n/**\n * Builds the event schedule that relation dates should be matched against.\n *\n * Manual statuses (`MANUAL_DATE_STATUSES`: CANCELED and RE_SCHEDULED) stay on\n * the live `Event.dateTime` for visibility, but a relation date pointing at\n * them is treated as removed. Archived (ended) slots are appended separately\n * so relation dates for archived history still match instead of being wrongly\n * marked canceled.\n *\n * Shared by the event-update relation sync and the cron orphan-date cleanup so\n * both use the same archive-aware schedule.\n */\nexport function buildRelationEligibleEventSchedule<\n T extends { dateStatus?: EnumEventDateStatus | null },\n>(liveDateTime: T[], archivedDateTime: T[] = []): T[] {\n const eligible = liveDateTime.filter(\n (slot) => !slot.dateStatus || !MANUAL_DATE_STATUSES.has(slot.dateStatus),\n );\n return [...eligible, ...archivedDateTime];\n}\n\n/**\n * Determines the dateStatus for a future event date\n * @param startDateTime The start date-time of the event\n * @param now The current date-time\n * @returns The appropriate EnumEventDateStatus\n */\nexport function getFutureEventStatus(\n startDateTime: dayjs.Dayjs,\n now: dayjs.Dayjs,\n): EnumEventDateStatus {\n const hoursUntilStart = startDateTime.diff(now, \"hour\", true);\n if (hoursUntilStart > 0 && hoursUntilStart <= 4) {\n return EnumEventDateStatus.STARTING_SOON;\n }\n\n if (startDateTime.isSame(now, \"day\")) {\n return EnumEventDateStatus.TODAY;\n }\n\n if (startDateTime.isSame(now.add(1, \"day\"), \"day\")) {\n return EnumEventDateStatus.TOMORROW;\n }\n\n if (startDateTime.isSame(now, \"isoWeek\")) {\n return EnumEventDateStatus.THIS_WEEK;\n }\n\n if (startDateTime.isSame(now.add(1, \"week\"), \"isoWeek\")) {\n return EnumEventDateStatus.NEXT_WEEK;\n }\n\n return EnumEventDateStatus.UPCOMING;\n}\n\n/**\n * Updates the dateStatus field for a single DateTimeType object based on the current date/time.\n * Manual statuses (CANCELED, RE_SCHEDULED) are preserved and returned as-is.\n * @param dateTime The date-time object to update\n * @returns A new object with an updated dateStatus value, or the original\n * object when the stored status is a manual override\n */\nexport function updateSingleDateTimeStatus<T extends DateTimeType>(\n dateTime: T,\n now: dayjs.Dayjs = dayjs(),\n): T {\n if (MANUAL_DATE_STATUSES.has(dateTime.dateStatus)) {\n return dateTime;\n }\n\n // Parse start and end date-time\n const dateTimeFormat = `${dateFormat} ${timeFormat}`;\n const startDateTime = dayjs(\n `${dateTime.startDate} ${dateTime.startTime}`,\n dateTimeFormat,\n true,\n );\n const endDateTime = dayjs(\n `${dateTime.endDate} ${dateTime.endTime}`,\n dateTimeFormat,\n true,\n );\n\n // Skip if dates are invalid\n if (!startDateTime.isValid() || !endDateTime.isValid()) {\n return {\n ...dateTime,\n dateStatus: EnumEventDateStatus.INVALID,\n };\n }\n\n if (endDateTime.isBefore(startDateTime)) {\n return {\n ...dateTime,\n dateStatus: EnumEventDateStatus.INVALID,\n };\n }\n\n let dateStatus: EnumEventDateStatus;\n\n if (endDateTime.isAfter(now)) {\n if (startDateTime.isAfter(now)) {\n // Event has not started yet\n dateStatus = getFutureEventStatus(startDateTime, now);\n } else {\n // Event is in progress\n dateStatus = EnumEventDateStatus.STARTED;\n }\n } else {\n // Event has ended\n dateStatus = EnumEventDateStatus.ENDED;\n }\n\n return {\n ...dateTime,\n dateStatus,\n };\n}\n","import dayjs from \"dayjs\";\nimport customParseFormat from \"dayjs/plugin/customParseFormat.js\";\nimport isSameOrAfter from \"dayjs/plugin/isSameOrAfter.js\";\nimport timezone from \"dayjs/plugin/timezone.js\";\nimport utc from \"dayjs/plugin/utc.js\";\n\nimport { EnumEventDateStatus } from \"src/enums\";\n\nimport { dateFormat, timeFormat, isoDateFormat } from \"./dateFormats\";\nimport {\n FUTURE_EVENT_DATE_STATUSES,\n EVENT_DATE_STATUS_LABELS,\n} from \"./eventDateStatus\";\n\nexport { dateFormat, timeFormat, isoDateFormat };\n\n// Enable custom format parsing\ndayjs.extend(customParseFormat);\ndayjs.extend(utc);\ndayjs.extend(timezone);\ndayjs.extend(isSameOrAfter);\n\nexport const NZ_TZ = \"Pacific/Auckland\";\n\nexport function toNZTime(date?: Date | string) {\n return date ? dayjs(date).tz(NZ_TZ) : dayjs().tz(NZ_TZ);\n}\n\n/** Start of the calendar day in Pacific/Auckland (daily games, streaks, etc.). */\nexport function nzStartOfDay(\n input?: Date | string | number | null,\n): dayjs.Dayjs {\n if (input == null) {\n return dayjs().tz(NZ_TZ).startOf(\"day\");\n }\n return dayjs.tz(input, NZ_TZ).startOf(\"day\");\n}\n\n/**\n * UTC-midnight of the given instant's NZ calendar day (today when omitted).\n * This is the canonical \"day key\" encoding used by ad bookings and metrics:\n * stored as a UTC Date, rendered with `.toISOString().slice(0, 10)`.\n */\nexport function nzDayKey(input?: Date | string | number | null): Date {\n const day =\n input == null\n ? dayjs().tz(NZ_TZ).format(isoDateFormat)\n : dayjs.tz(input, NZ_TZ).format(isoDateFormat);\n return new Date(`${day}T00:00:00Z`);\n}\n\n/**\n * Type guard for the canonical \"day key\" encoding: a valid Date that is\n * exactly UTC midnight. Accepts `unknown` so it can validate untrusted values\n * (GraphQL/JSON inputs) without a preceding cast.\n */\nexport function isUtcMidnight(value: unknown): value is Date {\n return (\n value instanceof Date &&\n !Number.isNaN(value.getTime()) &&\n value.getUTCHours() === 0 &&\n value.getUTCMinutes() === 0 &&\n value.getUTCSeconds() === 0 &&\n value.getUTCMilliseconds() === 0\n );\n}\n\ntype DateFormat = \"date\" | \"time\" | \"datetime\";\n\n/** Long-form date text shared by {@link formatDate} and {@link formatDateOptionLabel}. */\nconst DATE_DISPLAY_FORMAT = \"dddd, D MMMM, YYYY\";\n\n/**\n * Format a date string to a more readable format.\n * @param dateStr - the date string\n * @param timeStr - optional time string\n * @param display - 'date' | 'time' | 'datetime'\n * @returns formatted string based on display option\n */\nexport const formatDate = (\n dateStr: string,\n display: DateFormat = \"datetime\",\n timeStr?: string,\n) => {\n // Combine date and time into a single string if time is provided\n const dateTimeStr = timeStr ? `${dateStr} ${timeStr}` : dateStr;\n\n // Parse with formats\n const dateTime = timeStr\n ? dayjs(dateTimeStr, `${dateFormat} ${timeFormat}`)\n : dayjs(dateStr, dateFormat);\n\n // Format parts\n const formattedDate = dateTime.format(DATE_DISPLAY_FORMAT);\n const formattedTime = dateTime.format(\"h:mm a\");\n\n // Return based on display option\n switch (display) {\n case \"date\":\n return formattedDate;\n case \"time\":\n return formattedTime;\n case \"datetime\":\n return `${formattedDate} at ${formattedTime}`;\n default:\n return formattedDate;\n }\n};\n\n/**\n * Strictly parses a `dd-mm-yyyy` date. Malformed or partially matching input\n * (e.g. `2026-10-26`, or an impossible day like `31-02-2026`) yields an invalid\n * Dayjs instead of silently rolling into another date.\n */\nfunction parseStrictDate(dateStr: string): dayjs.Dayjs {\n return dayjs(dateStr, dateFormat, true);\n}\n\n/**\n * True when the parsed calendar day precedes today's day in `Pacific/Auckland`.\n * Compares day keys: `dayKey` is UTC-midnight of the parsed day and `nzDayKey()`\n * is UTC-midnight of today's NZ day, so neither depends on the host timezone.\n */\nfunction isDayBeforeNzToday(parsed: dayjs.Dayjs): boolean {\n const dayKey = new Date(`${parsed.format(isoDateFormat)}T00:00:00Z`);\n\n return dayKey.getTime() < nzDayKey().getTime();\n}\n\n/**\n * True when a `dd-mm-yyyy` date falls before today's calendar day in\n * `Pacific/Auckland`. The current day counts as not past, and values that do not\n * parse as a date are never treated as past.\n */\nexport function isPastDate(dateStr: string): boolean {\n const parsed = parseStrictDate(dateStr);\n\n return parsed.isValid() && isDayBeforeNzToday(parsed);\n}\n\n/**\n * Date-option label: the long-form date text, marked `(Past)` when the slot has\n * already happened. Used for the event/vendor date filter pills, which also list\n * historical relation dates.\n *\n * The label and the marker come from the same strict parse, so an impossible or\n * malformed date is returned verbatim rather than being rolled over into a real\n * date that would then contradict the `(Past)` decision.\n */\nexport function formatDateOptionLabel(dateStr: string): string {\n const parsed = parseStrictDate(dateStr);\n\n if (!parsed.isValid()) {\n return dateStr;\n }\n\n const label = parsed.format(DATE_DISPLAY_FORMAT);\n\n return isDayBeforeNzToday(parsed) ? `${label} (Past)` : label;\n}\n\n/**\n * Formats accepted for a stored slot. The canonical writer is\n * `dayjs(...).format(`${dateFormat} ${timeFormat}`)`; the variants cover legacy\n * rows with an unpadded hour or trailing seconds. Each is parsed **strictly**, so\n * impossible values (`31-02`, `25:00`) are rejected instead of rolling over into\n * a real instant.\n */\nconst SLOT_FORMATS = [\n `${dateFormat} ${timeFormat}`,\n `${dateFormat} H:mm`,\n `${dateFormat} ${timeFormat}:ss`,\n `${dateFormat} H:mm:ss`,\n];\n\nconst parseSlot = (date: {\n startDate?: string;\n startTime?: string;\n}): dayjs.Dayjs | null => {\n if (!date.startDate || !date.startTime) {\n return null;\n }\n\n const value = `${date.startDate} ${date.startTime}`;\n\n for (const format of SLOT_FORMATS) {\n const parsed = dayjs(value, format, true);\n\n if (parsed.isValid()) {\n return parsed;\n }\n }\n\n return null;\n};\n\n/**\n * Shared \"starts now or later\" rule for `dd-mm-yyyy` + `HH:mm` slots. `now` is\n * passed in so a caller filtering a list compares every slot against the same\n * instant. A slot with a missing or unparseable date/time is never future.\n */\nconst isFutureDateTime = (\n date: { startDate?: string; startTime?: string },\n now: dayjs.Dayjs,\n): boolean => {\n const slot = parseSlot(date);\n\n return slot ? slot.isSameOrAfter(now) : false;\n};\n\nexport const getCurrentAndFutureDates = <\n T extends { startDate?: string; startTime?: string },\n>(\n dates: T[],\n): T[] => {\n const now = dayjs(); // current date and time\n\n return dates.filter((dateObj) => isFutureDateTime(dateObj, now));\n};\n\n/**\n * True when the slot starts now or later. Status-agnostic: used to hide past\n * relation dates from reads while the records stay in the database.\n */\nexport const isFutureStartDate = (date: {\n startDate?: string;\n startTime?: string;\n}): boolean => isFutureDateTime(date, dayjs());\n\nexport const isFutureDatesBeforeThreshold = (\n date: {\n startDate?: string;\n startTime?: string;\n },\n minHoursFromNow: number,\n): boolean => {\n if (!date.startDate || !date.startTime) {\n return false;\n }\n\n const threshold = minHoursFromNow\n ? dayjs().add(minHoursFromNow, \"hour\")\n : dayjs().startOf(\"day\");\n\n const dateTime = dayjs(\n `${date.startDate} ${date.startTime}`,\n `${dateFormat} ${timeFormat}`,\n );\n\n return dateTime.isSameOrAfter(threshold);\n};\n\nexport const formatTimestamp = (timestamp: string) => {\n const formattedDate = toNZTime(timestamp).format(dateFormat);\n\n return formatDate(formattedDate, \"date\");\n};\n\nexport const isIsoDateString = (value: unknown): value is string => {\n return typeof value === \"string\" && !Number.isNaN(Date.parse(value));\n};\n\n/**\n * Sort an array of date strings by their proximity to the current date.\n * @param dates - The array of date strings to sort.\n * @returns - The sorted array of date strings.\n */\nexport function sortDatesChronologically<\n T extends { startDate?: string; startTime?: string },\n>(dates: T[]): T[] {\n if (!dates?.length) {\n return [];\n }\n\n return [...dates].sort((a, b) => {\n const dateTimeFormat = `${dateFormat} ${timeFormat}`;\n const dateA =\n a.startDate && a.startTime\n ? dayjs(`${a.startDate} ${a.startTime}`, dateTimeFormat)\n : null;\n const dateB =\n b.startDate && b.startTime\n ? dayjs(`${b.startDate} ${b.startTime}`, dateTimeFormat)\n : null;\n\n if (dateA && dateB) {\n return dateA.valueOf() - dateB.valueOf();\n }\n\n if (dateA) {\n return -1;\n }\n\n if (dateB) {\n return 1;\n }\n\n return 0;\n });\n}\n\n/**\n * Start dates of the given date/time slots, chronological and de-duplicated.\n * Slots that share a `startDate` (e.g. two stall slots on the same day) collapse\n * into a single entry.\n */\nexport function getUniqueStartDates(\n dates: Array<{ startDate?: string; startTime?: string }> | null | undefined,\n): string[] {\n const seenStartDates = new Set<string>();\n const uniqueStartDates: string[] = [];\n\n for (const { startDate } of sortDatesChronologically(dates ?? [])) {\n if (!startDate || seenStartDates.has(startDate)) {\n continue;\n }\n\n seenStartDates.add(startDate);\n uniqueStartDates.push(startDate);\n }\n\n return uniqueStartDates;\n}\n\n/**\n * UI filter options for user-activity event periods. Derived from the canonical\n * {@link FUTURE_EVENT_DATE_STATUSES} list, minus `STARTING_SOON` (a presentational\n * choice: \"starting soon\" is surfaced separately rather than as a filter).\n */\nexport const futureTimePeriods = FUTURE_EVENT_DATE_STATUSES.filter(\n (status) => status !== EnumEventDateStatus.STARTING_SOON,\n).map((status) => ({\n label: EVENT_DATE_STATUS_LABELS[status],\n value: status,\n}));\n","/** Path segments for relation share URLs — must match mobile `RelationTitle` values. */\nexport const RELATION_SHARE_INVITATION = \"invitation\" as const;\nexport const RELATION_SHARE_APPLICATION = \"application\" as const;\n\n/**\n * Bump when relation overlay SVG/layout changes so og:image URLs and API ETags\n * invalidate after deploy. Facebook caches preview JPEGs by exact URL — changing\n * this value forces crawlers to fetch a fresh image after server deploy.\n */\nexport const RELATION_OVERLAY_LAYOUT_VERSION = 43;\n\nexport const RELATION_SHARE_RESOURCE_TYPES = [\n RELATION_SHARE_INVITATION,\n RELATION_SHARE_APPLICATION,\n] as const;\n\nexport type RelationShareResourceType =\n (typeof RELATION_SHARE_RESOURCE_TYPES)[number];\n\nexport function isRelationShareResourceType(\n resourceType: string,\n): resourceType is RelationShareResourceType {\n return (RELATION_SHARE_RESOURCE_TYPES as readonly string[]).includes(\n resourceType,\n );\n}\n","import {\n RELATION_SHARE_APPLICATION,\n RELATION_SHARE_INVITATION,\n type RelationShareResourceType,\n} from \"./relationShareTypes\";\n\nexport {\n RELATION_SHARE_APPLICATION,\n RELATION_SHARE_INVITATION,\n RELATION_OVERLAY_LAYOUT_VERSION,\n RELATION_SHARE_RESOURCE_TYPES,\n isRelationShareResourceType,\n type RelationShareResourceType,\n} from \"./relationShareTypes\";\n\n/** Open Graph recommended preview size (must match admin `SHARE_OG_IMAGE_*`). */\nexport const SOCIAL_IMAGE_WIDTH = 1200;\n\n/** Open Graph recommended preview size (must match admin `SHARE_OG_IMAGE_*`). */\nexport const SOCIAL_IMAGE_HEIGHT = 630;\n\n/** Keep in sync with `CLUEMART_MAIN_DOMAIN_URL` in utils (avoid utils↔sharing import cycle). */\nexport const SHARE_SITE_URL = \"https://cluemart.co.nz\";\n\n/** Calendar marker for invitation market-dates share copy (U+1F4C5 📅). */\nexport const SHARE_CALENDAR_ICON = \"\\u{1F4C5}\";\n\nexport const SHARE_MARKET_DATES_SECTION_HEADING = `${SHARE_CALENDAR_ICON} Market dates:`;\n\n/** Check mark for invitation requirements share copy (U+2705 ✅). */\nexport const SHARE_CHECKMARK_ICON = \"\\u{2705}\";\n\nexport const SHARE_REQUIREMENTS_SECTION_HEADING = `${SHARE_CHECKMARK_ICON} Requirements:`;\n\n/** Information marker for tags share copy (U+2139 ℹ️). */\nexport const SHARE_INFO_ICON = \"\\u{2139}\\u{FE0F}\";\n\nexport const SHARE_TAGS_SECTION_HEADING = `${SHARE_INFO_ICON} Tags:`;\n\n/** Standalone line when `rainOrShine` is true (invitation share + OG section parsing). */\nexport const SHARE_RAIN_OR_SHINE_LINE = \"Rain or Shine\";\n\n/** Standalone line when `foodTruck` is true (application share + OG section parsing). */\nexport const SHARE_FOOD_TRUCK_LINE = \"Food Truck\";\n\n/** Prefix for application compliance line (share text + OG section parsing). */\nexport const SHARE_COMPLIANCE_PREFIX = `${SHARE_CHECKMARK_ICON} Compliance:`;\n\n/** Label tag for application categories share copy (U+1F3F7 🏷️). */\nexport const SHARE_CATEGORY_ICON = \"\\u{1F3F7}\\u{FE0F}\";\n\nexport const SHARE_CATEGORIES_SECTION_HEADING = `${SHARE_CATEGORY_ICON} Categories:`;\n\n/** Straight ruler for application stall-size share copy (U+1F4CF 📏). */\nexport const SHARE_RULER_ICON = \"\\u{1F4CF}\";\n\n/** Prefix for application stall-size line (share text + OG section parsing). */\nexport const SHARE_STALL_SIZE_PREFIX = `${SHARE_RULER_ICON} Stall size:`;\n\n/** Dollar marker for price-range share copy (U+0024 $ — text, not emoji, so it stays black). */\nexport const SHARE_DOLLAR_ICON = \"$\";\n\n/** Prefix for application price-range line (share text + OG section parsing). */\nexport const SHARE_PRICE_RANGE_PREFIX = `${SHARE_DOLLAR_ICON} Price range:`;\n\n/** Alarm clock for invitation application-deadline share copy (U+23F0 ⏰). */\nexport const SHARE_CLOCK_ICON = \"\\u{23F0}\";\n\n/** Prefix for the invitation application-deadline sentence (share text + OG section parsing). */\nexport const SHARE_APPLICATION_DEADLINE_PREFIX = `${SHARE_CLOCK_ICON} Application deadline:`;\n\n/** First line of `og:description` (and legacy share-message text). */\nexport const SHARE_MORE_INFO_LINE = \"Shared from ClueMart\";\n\nexport const DEFAULT_SHARE_OG_IMAGE = `${SHARE_SITE_URL}/assets/logo.webp`;\n\nexport const RESOURCE_SHARE_TYPES = [\n \"market\",\n \"stallholder\",\n \"partner\",\n \"affiliate\",\n \"school\",\n] as const;\n\nexport type ResourceShareType = (typeof RESOURCE_SHARE_TYPES)[number];\n\nexport const POST_SHARE_RESOURCE_TYPES = [\n \"market_faces\",\n \"clue_bites\",\n \"play_and_win\",\n] as const;\n\nexport type PostShareResourceType = (typeof POST_SHARE_RESOURCE_TYPES)[number];\n\nexport type ShareResourceType =\n ResourceShareType | PostShareResourceType | RelationShareResourceType;\n\nexport const SHARE_RESOURCE_LABEL: Record<ShareResourceType, string> = {\n [RELATION_SHARE_APPLICATION]: \"Application\",\n [RELATION_SHARE_INVITATION]: \"Invitation\",\n affiliate: \"Affiliate\",\n clue_bites: \"Clue Bites\",\n market_faces: \"Market Faces\",\n market: \"Market\",\n partner: \"Partner\",\n play_and_win: \"Play & Win\",\n school: \"School\",\n stallholder: \"Stallholder\",\n};\n\nexport function isPostShareResourceType(\n resourceType: ShareResourceType,\n): resourceType is PostShareResourceType {\n return (POST_SHARE_RESOURCE_TYPES as readonly string[]).includes(\n resourceType,\n );\n}\n","import { SHARE_MORE_INFO_LINE } from \"./constants\";\n\n/** Blank line between share sections — title, address, dates, URL, etc. */\nexport const SHARE_DESCRIPTION_SECTION_BREAK = \"\\n\\n\";\n\n/**\n * Line break for `og:description` meta content. Use instead of `\\n` — Next.js\n * HTML serialisation collapses literal newlines to spaces; Facebook reads `&#10;`.\n */\nexport const OG_DESCRIPTION_LINE_BREAK = \"&#10;\";\n\n/** Joins OG description sections for Facebook link cards (and Twitter). */\nexport function joinShareOgDescriptionSections(\n sections: ReadonlyArray<string | false | null | undefined>,\n): string {\n return sections\n .map((section) => (typeof section === \"string\" ? section.trim() : \"\"))\n .filter((section): section is string => section.length > 0)\n .map((section) => section.replace(/\\n/g, OG_DESCRIPTION_LINE_BREAK))\n .join(OG_DESCRIPTION_LINE_BREAK);\n}\n\nexport function joinShareDescriptionSections(\n sections: ReadonlyArray<string | false | null | undefined>,\n): string {\n return sections\n .filter((section): section is string => Boolean(section))\n .join(SHARE_DESCRIPTION_SECTION_BREAK);\n}\n\n/** Prepends the standard share branding line when assembling display text. */\nexport function appendShareMoreInfoLine(description: string): string {\n const trimmed = description.trim();\n if (trimmed.startsWith(SHARE_MORE_INFO_LINE)) {\n return trimmed;\n }\n if (trimmed.length === 0) {\n return SHARE_MORE_INFO_LINE;\n }\n return joinShareDescriptionSections([SHARE_MORE_INFO_LINE, trimmed]);\n}\n","import { joinShareOgDescriptionSections } from \"./joinShareDescriptionSections\";\n\nexport {\n joinShareOgDescriptionSections,\n OG_DESCRIPTION_LINE_BREAK,\n SHARE_DESCRIPTION_SECTION_BREAK,\n} from \"./joinShareDescriptionSections\";\n\n/** Trims share text and collapses spaces per line; preserves `\\n\\n` section breaks. */\nconst LEADING_WHITESPACE = /^[ \\t]*/;\n\nexport function normalizeShareText(value: string | null | undefined): string {\n if (value == null) {\n return \"\";\n }\n return value\n .trim()\n .split(\"\\n\")\n .map((line) => {\n const leading = LEADING_WHITESPACE.exec(line)?.[0] ?? \"\";\n const rest = line\n .slice(leading.length)\n .trim()\n .replace(/[ \\t]{2,}/g, \" \");\n return leading + rest;\n })\n .join(\"\\n\")\n .replace(/\\n{3,}/g, \"\\n\\n\");\n}\n\nexport function splitShareDescriptionSections(value: string): string[] {\n const trimmed = normalizeShareText(value);\n if (!trimmed) {\n return [];\n }\n\n if (/\\n\\n/.test(trimmed)) {\n return trimmed\n .split(/\\n\\n+/)\n .map((section) => normalizeShareText(section))\n .filter((section) => section.length > 0);\n }\n\n if (/\\n/.test(trimmed)) {\n return trimmed\n .split(/\\n+/)\n .map((section) => normalizeShareText(section))\n .filter((section) => section.length > 0);\n }\n\n return [trimmed];\n}\n\n/**\n * Formats share descriptions for Open Graph / Twitter meta tags.\n * Sections are joined with {@link OG_DESCRIPTION_LINE_BREAK} (`&#10;`).\n */\nexport function normalizeShareOgDescription(value: string): string {\n return joinShareOgDescriptionSections(splitShareDescriptionSections(value));\n}\n\n/**\n * Open Graph / Twitter description from pre-built section strings.\n * Prefer {@link joinShareOgDescriptionSections} when sections are already known.\n */\nexport function buildShareOgDescriptionFromSections(\n sections: ReadonlyArray<string | false | null | undefined>,\n): string {\n return joinShareOgDescriptionSections(sections);\n}\n\n/** Open Graph / Twitter description — body sections only (`og:title` is separate). */\nexport function buildShareOgDescription(\n _title: string,\n description: string,\n): string {\n return joinShareOgDescriptionSections(\n splitShareDescriptionSections(description),\n );\n}\n\n/**\n * Strips emoji and symbols that do not render reliably in overlay SVG fonts.\n * Keeps letters, numbers, whitespace, and common sentence punctuation.\n */\nexport function stripOverlaySubtitleSpecialChars(text: string): string {\n const regex = /[^\\p{L}\\p{M}\\p{N}\\s.,!?'\"&\\-():;/]/gu;\n const result = text\n .replace(/\\p{Extended_Pictographic}/gu, \"\")\n .replace(regex, \"\");\n return result.trim();\n}\n"],"mappings":"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","names":["dayjs","customParseFormat","dayjs","customParseFormat"]}
@@ -44,10 +44,14 @@ import {
44
44
  VENDOR_PRODUCT_LIST_FIELDS_FRAGMENT
45
45
  } from "./chunk-TKSBCN3N.mjs";
46
46
  import {
47
+ AD_METRIC_IMPRESSION_RETRY_DELAY_MS,
47
48
  DEFAULT_PAGE_SIZE,
48
49
  accumulateItems,
49
- defaultGetId
50
- } from "./chunk-7DQBUZXY.mjs";
50
+ buildAdMetricSlotKey,
51
+ defaultGetId,
52
+ generateAdClickId,
53
+ getAdMetricRotationSlot
54
+ } from "./chunk-YCUKTVVA.mjs";
51
55
 
52
56
  // src/graphql/mutations/vendor.ts
53
57
  import { gql } from "@apollo/client";
@@ -2200,6 +2204,7 @@ var useSelectStandardPackage = () => {
2200
2204
 
2201
2205
  // src/graphql/hooks/ad/hooksMutation.ts
2202
2206
  import { useMutation as useMutation11 } from "@apollo/client";
2207
+ import { useCallback, useEffect, useRef } from "react";
2203
2208
 
2204
2209
  // src/graphql/mutations/ad.ts
2205
2210
  import { gql as gql21 } from "@apollo/client";
@@ -2365,6 +2370,15 @@ var GET_AD_SETTINGS = gql22`
2365
2370
  }
2366
2371
  }
2367
2372
  `;
2373
+ var GET_AD_METRIC_TOTALS = gql22`
2374
+ query getAdMetricTotals($filter: AdMetricTotalsFilterInputGraphQLType) {
2375
+ adMetricTotals(filter: $filter) {
2376
+ adId
2377
+ clicks
2378
+ impressions
2379
+ }
2380
+ }
2381
+ `;
2368
2382
 
2369
2383
  // src/graphql/mutations/adBooking.ts
2370
2384
  var CREATE_AD_CHECKOUT_MUTATION = gql23`
@@ -2481,14 +2495,147 @@ var useRecordAdClick = () => {
2481
2495
  recordAdClick
2482
2496
  };
2483
2497
  };
2498
+ var defaultAdMetricErrorHandler = (message, error) => {
2499
+ console.error(message, error);
2500
+ };
2501
+ var useAdMetricsReporter = (input, onError = defaultAdMetricErrorHandler) => {
2502
+ const { recordAdImpression } = useRecordAdImpression();
2503
+ const { recordAdClick } = useRecordAdClick();
2504
+ const reportedSlotRef = useRef({
2505
+ keys: /* @__PURE__ */ new Set(),
2506
+ slot: -1
2507
+ });
2508
+ const inFlightRef = useRef(false);
2509
+ const disposedRef = useRef(false);
2510
+ const safeOnError = useCallback(
2511
+ (message, error) => {
2512
+ try {
2513
+ onError(message, error);
2514
+ } catch {
2515
+ }
2516
+ },
2517
+ [onError]
2518
+ );
2519
+ useEffect(() => {
2520
+ disposedRef.current = false;
2521
+ return () => {
2522
+ disposedRef.current = true;
2523
+ };
2524
+ }, []);
2525
+ const reportImpression = useCallback(
2526
+ async (opts) => {
2527
+ if (inFlightRef.current || disposedRef.current) return false;
2528
+ const now = opts?.nowMs ?? Date.now();
2529
+ const slot = getAdMetricRotationSlot(now);
2530
+ const key = buildAdMetricSlotKey(input.adId, input.placement, slot);
2531
+ const reported = reportedSlotRef.current;
2532
+ if (reported.slot !== slot) {
2533
+ reported.slot = slot;
2534
+ reported.keys.clear();
2535
+ }
2536
+ if (reported.keys.has(key)) return false;
2537
+ inFlightRef.current = true;
2538
+ try {
2539
+ const result = await recordAdImpression({
2540
+ variables: {
2541
+ input: {
2542
+ adId: input.adId,
2543
+ placement: input.placement,
2544
+ platform: input.platform,
2545
+ region: input.region
2546
+ }
2547
+ }
2548
+ });
2549
+ if (result.data?.recordAdImpression?.data !== true) {
2550
+ return false;
2551
+ }
2552
+ reported.keys.add(key);
2553
+ return true;
2554
+ } catch (error) {
2555
+ safeOnError(
2556
+ "Ad impression report failed; retrying within the rotation.",
2557
+ error
2558
+ );
2559
+ if (!disposedRef.current && getAdMetricRotationSlot(Date.now()) === slot) {
2560
+ await new Promise(
2561
+ (resolve) => setTimeout(resolve, AD_METRIC_IMPRESSION_RETRY_DELAY_MS)
2562
+ );
2563
+ if (!disposedRef.current && getAdMetricRotationSlot(Date.now()) === slot) {
2564
+ try {
2565
+ const retryResult = await recordAdImpression({
2566
+ variables: {
2567
+ input: {
2568
+ adId: input.adId,
2569
+ placement: input.placement,
2570
+ platform: input.platform,
2571
+ region: input.region
2572
+ }
2573
+ }
2574
+ });
2575
+ if (retryResult.data?.recordAdImpression?.data !== true) {
2576
+ return false;
2577
+ }
2578
+ reported.keys.add(key);
2579
+ return true;
2580
+ } catch (retryError) {
2581
+ safeOnError("Ad impression retry failed.", retryError);
2582
+ }
2583
+ }
2584
+ }
2585
+ return false;
2586
+ } finally {
2587
+ inFlightRef.current = false;
2588
+ }
2589
+ },
2590
+ [
2591
+ input.adId,
2592
+ input.placement,
2593
+ input.platform,
2594
+ input.region,
2595
+ safeOnError,
2596
+ recordAdImpression
2597
+ ]
2598
+ );
2599
+ const reportClick = useCallback(
2600
+ (adIdOverride) => {
2601
+ try {
2602
+ void recordAdClick({
2603
+ variables: {
2604
+ input: {
2605
+ adId: adIdOverride ?? input.adId,
2606
+ clickId: generateAdClickId(),
2607
+ placement: input.placement,
2608
+ platform: input.platform,
2609
+ region: input.region
2610
+ }
2611
+ }
2612
+ }).catch((error) => {
2613
+ safeOnError("Ad click report failed.", error);
2614
+ });
2615
+ } catch (error) {
2616
+ safeOnError("Ad click report failed.", error);
2617
+ }
2618
+ },
2619
+ [
2620
+ input.adId,
2621
+ input.placement,
2622
+ input.platform,
2623
+ input.region,
2624
+ safeOnError,
2625
+ recordAdClick
2626
+ ]
2627
+ );
2628
+ return { reportClick, reportImpression };
2629
+ };
2484
2630
 
2485
2631
  // src/graphql/hooks/ad/hooksQuery.ts
2486
2632
  import { useQuery as useQuery6 } from "@apollo/client";
2487
- var useGetAds = () => {
2633
+ var useGetAds = (skip = false) => {
2488
2634
  const { data, loading, error, refetch } = useQuery6(
2489
2635
  GET_ADS,
2490
2636
  {
2491
- fetchPolicy: "no-cache"
2637
+ fetchPolicy: "no-cache",
2638
+ skip
2492
2639
  }
2493
2640
  );
2494
2641
  return {
@@ -2524,6 +2671,19 @@ var useGetAdsByRegion = (region, platform, showOn) => {
2524
2671
  refetch
2525
2672
  };
2526
2673
  };
2674
+ var useGetAdMetricTotals = (filter, skip = false) => {
2675
+ const { data, loading, error, refetch } = useQuery6(GET_AD_METRIC_TOTALS, {
2676
+ fetchPolicy: "no-cache",
2677
+ skip,
2678
+ variables: filter ? { filter } : void 0
2679
+ });
2680
+ return {
2681
+ adMetricTotals: data?.adMetricTotals || [],
2682
+ error,
2683
+ loading,
2684
+ refetch
2685
+ };
2686
+ };
2527
2687
 
2528
2688
  // src/graphql/hooks/resourceActivities/hooksMutation.ts
2529
2689
  import { useMutation as useMutation12 } from "@apollo/client";
@@ -4138,7 +4298,7 @@ var useGetCoupon = (_id) => {
4138
4298
 
4139
4299
  // src/graphql/hooks/usePaginatedList.ts
4140
4300
  import { useQuery as useQuery17 } from "@apollo/client";
4141
- import { useCallback, useEffect, useRef, useState } from "react";
4301
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState } from "react";
4142
4302
  function usePaginatedList({
4143
4303
  document,
4144
4304
  getItems,
@@ -4157,7 +4317,7 @@ function usePaginatedList({
4157
4317
  const [hasMore, setHasMore] = useState(!skip);
4158
4318
  const [hasSettled, setHasSettled] = useState(false);
4159
4319
  const variablesKey = JSON.stringify(variables ?? null);
4160
- useEffect(() => {
4320
+ useEffect2(() => {
4161
4321
  setItems([]);
4162
4322
  setHasMore(!skip);
4163
4323
  setHasSettled(false);
@@ -4168,14 +4328,14 @@ function usePaginatedList({
4168
4328
  skip,
4169
4329
  variables: { ...variables, options: { limit: pageSize + 1, offset } }
4170
4330
  });
4171
- const refetchRef = useRef(refetch);
4331
+ const refetchRef = useRef2(refetch);
4172
4332
  refetchRef.current = refetch;
4173
- const variablesRef = useRef(variables);
4333
+ const variablesRef = useRef2(variables);
4174
4334
  variablesRef.current = variables;
4175
4335
  const rawPage = getItems(data);
4176
4336
  const page = rawPage.length > pageSize ? rawPage.slice(0, pageSize) : rawPage;
4177
4337
  const hasMorePage = rawPage.length > pageSize;
4178
- useEffect(() => {
4338
+ useEffect2(() => {
4179
4339
  if (loading || skip || error) {
4180
4340
  return;
4181
4341
  }
@@ -4191,7 +4351,7 @@ function usePaginatedList({
4191
4351
  setHasSettled(true);
4192
4352
  setHasMore(hasMorePage);
4193
4353
  }, [loading, skip, error, page, hasMorePage, hasSettled, pageSize, getId]);
4194
- const loadMore = useCallback(() => {
4354
+ const loadMore = useCallback2(() => {
4195
4355
  if (loading || skip) {
4196
4356
  return;
4197
4357
  }
@@ -4207,7 +4367,7 @@ function usePaginatedList({
4207
4367
  }
4208
4368
  setOffset(offset + pageSize);
4209
4369
  }, [loading, skip, error, hasSettled, hasMore, offset, pageSize]);
4210
- const refresh = useCallback(() => {
4370
+ const refresh = useCallback2(() => {
4211
4371
  setItems([]);
4212
4372
  setHasMore(!skip);
4213
4373
  setHasSettled(false);
@@ -4330,6 +4490,7 @@ export {
4330
4490
  GET_PENDING_AD_BOOKINGS,
4331
4491
  GET_AD_DAILY_PRICES,
4332
4492
  GET_AD_SETTINGS,
4493
+ GET_AD_METRIC_TOTALS,
4333
4494
  CREATE_AD_CHECKOUT_MUTATION,
4334
4495
  APPROVE_AD_BOOKING_MUTATION,
4335
4496
  UPDATE_AD_DAILY_PRICE_MUTATION,
@@ -4341,9 +4502,11 @@ export {
4341
4502
  useDeleteAd,
4342
4503
  useRecordAdImpression,
4343
4504
  useRecordAdClick,
4505
+ useAdMetricsReporter,
4344
4506
  useGetAds,
4345
4507
  useGetAd,
4346
4508
  useGetAdsByRegion,
4509
+ useGetAdMetricTotals,
4347
4510
  useCreateResourceActivity,
4348
4511
  useGetResourceActivity,
4349
4512
  useCreateCheckoutSession,
@@ -4399,4 +4562,4 @@ export {
4399
4562
  useGetCoupon,
4400
4563
  usePaginatedList
4401
4564
  };
4402
- //# sourceMappingURL=chunk-2RHUGVV3.mjs.map
4565
+ //# sourceMappingURL=chunk-WOBG6I4T.mjs.map