test
1 month ago
analyticsApi.js
1 month ago
classnames.js
1 month ago
dateUtils.js
1 month ago
formatters.js
1 month ago
helpers.js
1 month ago
icons.js
1 month ago
index.js
1 month ago
pluginUtils.js
1 month ago
transformers.js
1 month ago
viewportBoundary.js
1 month ago
dateUtils.js
203 lines
| 1 | import { format as format_date, startOfDay, subDays, subMonths } from "date-fns"; |
| 2 | |
| 3 | const { __ } = wp.i18n; |
| 4 | |
| 5 | /** Shared start date for "all time" analytics queries */ |
| 6 | export const ALL_TIME_START = "2020-01-01T00:00:00.000Z"; |
| 7 | |
| 8 | /** |
| 9 | * Day count used as the dashboard's default analytics window. Centralised |
| 10 | * here so every surface (picker, mount-fetch, detail-page fallbacks) stays |
| 11 | * in sync if the default ever shifts again. |
| 12 | */ |
| 13 | export const DEFAULT_ANALYTICS_DAYS = 90; |
| 14 | |
| 15 | /** |
| 16 | * All-time range used as the dashboard's default analytics window. |
| 17 | * |
| 18 | * `from` is the fixed sentinel ALL_TIME_START — byte-identical to what |
| 19 | * `useTopPerforming.js` already sends, so the wire payload is unchanged |
| 20 | * for surfaces that previously fell back to all-time. |
| 21 | * |
| 22 | * `to` is today's start-of-day so it lines up with `getLastNDays(N).to` |
| 23 | * and the per-day boundaries Force UI's preset matcher expects. |
| 24 | * |
| 25 | * @returns {{ from: Date, to: Date }} |
| 26 | */ |
| 27 | export const getAllTimeRange = () => ({ |
| 28 | from: new Date(ALL_TIME_START), |
| 29 | to: startOfDay(new Date()), |
| 30 | }); |
| 31 | |
| 32 | /** |
| 33 | * Custom preset list passed to Force UI's `<DatePicker variant="presets">`. |
| 34 | * |
| 35 | * Force UI's built-in presets do not include "All Time", and supplying |
| 36 | * a `presets` prop fully overrides the built-in list — so once we want |
| 37 | * "All Time" available we must declare every preset we wish to expose. |
| 38 | * |
| 39 | * Order here is the order rendered in the dropdown. |
| 40 | * |
| 41 | * @returns {{ label: string, range: { from: Date, to: Date } }[]} |
| 42 | */ |
| 43 | export const getAnalyticsPresets = () => { |
| 44 | const today = startOfDay(new Date()); |
| 45 | return [ |
| 46 | { |
| 47 | label: __("All Time", "presto-player"), |
| 48 | range: getAllTimeRange(), |
| 49 | }, |
| 50 | { |
| 51 | label: __("Last 7 days", "presto-player"), |
| 52 | range: { from: startOfDay(subDays(today, 6)), to: today }, |
| 53 | }, |
| 54 | { |
| 55 | label: __("Last 30 days", "presto-player"), |
| 56 | range: { from: startOfDay(subDays(today, 29)), to: today }, |
| 57 | }, |
| 58 | { |
| 59 | label: __("Last 90 days", "presto-player"), |
| 60 | range: { from: startOfDay(subDays(today, 89)), to: today }, |
| 61 | }, |
| 62 | { |
| 63 | label: __("Last 12 months", "presto-player"), |
| 64 | range: { from: startOfDay(subMonths(today, 12)), to: today }, |
| 65 | }, |
| 66 | ]; |
| 67 | }; |
| 68 | |
| 69 | /** |
| 70 | * Formats a date using date-fns with error handling |
| 71 | * |
| 72 | * @param {Date|string|number} date - The date to format |
| 73 | * @param {string} dateFormat - The format string (default: "yyyy-MM-dd") |
| 74 | * @returns {string} The formatted date or error message |
| 75 | * |
| 76 | * @example |
| 77 | * format(new Date(), "MMM dd, yyyy") // Returns: "Jan 21, 2026" |
| 78 | * format("invalid", "yyyy-MM-dd") // Returns: "No Date" |
| 79 | */ |
| 80 | export const format = (date, dateFormat = "yyyy-MM-dd") => { |
| 81 | try { |
| 82 | if (!date || isNaN(new Date(date).getTime())) { |
| 83 | throw new Error(__("Invalid Date", "presto-player")); |
| 84 | } |
| 85 | return format_date(new Date(date), dateFormat); |
| 86 | } catch (error) { |
| 87 | return __("No Date", "presto-player"); |
| 88 | } |
| 89 | }; |
| 90 | |
| 91 | /** |
| 92 | * Formats X-axis labels for charts. The year is intentionally omitted — |
| 93 | * the tooltip surfaces the full "MMM dd, yyyy" date on hover, so adding |
| 94 | * it to every tick just steals horizontal space and forces Recharts to |
| 95 | * truncate labels. |
| 96 | * |
| 97 | * @param {string} tickItem - The tick item to format |
| 98 | * @returns {string} The formatted date string |
| 99 | */ |
| 100 | export const formatXAxis = (tickItem) => { |
| 101 | return format(new Date(tickItem), "MMM dd"); |
| 102 | }; |
| 103 | |
| 104 | /** |
| 105 | * Format a Date as the calendar-day-anchored ISO datetime the analytics REST |
| 106 | * endpoints expect: "yyyy-MM-ddT00:00:00.000Z" — the user's local calendar |
| 107 | * day, re-anchored at UTC midnight. |
| 108 | * |
| 109 | * Use this for both the `start` and `end` query args. The backend |
| 110 | * (`Visit.php`) discards the time-of-day and rebuilds 00:00:00 / 23:59:59 |
| 111 | * windows itself — only the date portion is load-bearing on the wire. |
| 112 | * |
| 113 | * Why not `.toISOString()`? It serialises midnight-local as the previous |
| 114 | * UTC day for any timezone east of UTC, masking today's records. |
| 115 | * |
| 116 | * Why not bare "yyyy-MM-dd"? The REST controllers declare every start/end |
| 117 | * arg as `format: date-time`, and `rest_parse_date()` rejects bare dates |
| 118 | * with 400. |
| 119 | * |
| 120 | * @param {Date|string|number} date |
| 121 | * @returns {string} e.g. "2026-05-07T00:00:00.000Z" |
| 122 | */ |
| 123 | export const toAnalyticsDate = (date) => |
| 124 | `${format(date, "yyyy-MM-dd")}T00:00:00.000Z`; |
| 125 | |
| 126 | /** |
| 127 | * Gets a date range for the last N days, inclusive of today. |
| 128 | * |
| 129 | * Boundaries are anchored at start-of-day so the result is `getTime()`-equal |
| 130 | * to Force UI's built-in DatePicker presets (e.g. "Last 30 Days"), which |
| 131 | * means the matching preset highlights when this range is the selected value. |
| 132 | * |
| 133 | * @param {number} days - The number of days in the window (e.g. 30 = today + 29 prior days) |
| 134 | * @returns {Object} Object with 'from' and 'to' Date objects (both at 00:00 local time) |
| 135 | * |
| 136 | * @example |
| 137 | * getLastNDays(7) // { from: startOfDay(today - 6), to: startOfDay(today) } |
| 138 | * getLastNDays(30) // { from: startOfDay(today - 29), to: startOfDay(today) } |
| 139 | */ |
| 140 | export const getLastNDays = (days) => { |
| 141 | if (isNaN(days)) { |
| 142 | return { |
| 143 | from: "", |
| 144 | to: "", |
| 145 | }; |
| 146 | } |
| 147 | const today = startOfDay(new Date()); |
| 148 | return { |
| 149 | from: startOfDay(subDays(today, days - 1)), |
| 150 | to: today, |
| 151 | }; |
| 152 | }; |
| 153 | |
| 154 | /** |
| 155 | * Formats selected date range for display |
| 156 | * |
| 157 | * @param {Object} selectedDatesForChart - Object with 'from' and 'to' dates |
| 158 | * @returns {string} The formatted date range string |
| 159 | * |
| 160 | * @example |
| 161 | * getSelectedDate({ from: new Date('2026-01-01'), to: new Date('2026-01-15') }) |
| 162 | * // Returns: "01/01/2026 - 01/15/2026" |
| 163 | */ |
| 164 | export const getSelectedDate = (selectedDatesForChart) => { |
| 165 | if (!selectedDatesForChart.from) { |
| 166 | return ""; |
| 167 | } |
| 168 | if (!selectedDatesForChart.to) { |
| 169 | return `${format(selectedDatesForChart.from, "MM/dd/yyyy")}`; |
| 170 | } |
| 171 | return `${format(selectedDatesForChart.from, "MM/dd/yyyy")} - ${format( |
| 172 | selectedDatesForChart.to, |
| 173 | "MM/dd/yyyy" |
| 174 | )}`; |
| 175 | }; |
| 176 | |
| 177 | /** |
| 178 | * Renders the trigger label for an analytics date-range picker: |
| 179 | * - if `range` matches a known preset by calendar-day equality → preset label |
| 180 | * - else if `range` is a usable from/to → formatted "MM/dd/yyyy - MM/dd/yyyy" |
| 181 | * - else → empty string |
| 182 | * |
| 183 | * Day-string equality (rather than `getTime()`) is used so a `range` |
| 184 | * computed in a previous render or context still matches the preset |
| 185 | * built fresh this render. |
| 186 | * |
| 187 | * @param {{ from: Date, to: Date }} range |
| 188 | * @returns {string} |
| 189 | */ |
| 190 | export const getRangeLabel = (range) => { |
| 191 | if (!range?.from) return ""; |
| 192 | const sameDay = (a, b) => |
| 193 | a && b && format(a, "yyyy-MM-dd") === format(b, "yyyy-MM-dd"); |
| 194 | const presets = getAnalyticsPresets(); |
| 195 | const match = presets.find( |
| 196 | (p) => |
| 197 | sameDay(p.range.from, range.from) && |
| 198 | sameDay(p.range.to, range.to ?? range.from) |
| 199 | ); |
| 200 | return match ? match.label : getSelectedDate(range); |
| 201 | }; |
| 202 | |
| 203 |