PluginProbe ʕ •ᴥ•ʔ
Presto Player / trunk
Presto Player vtrunk
4.3.0 4.2.4 4.2.3 4.2.2 4.2.0 4.2.1 trunk 1.10.0 1.10.1 1.10.2 1.11.0 1.12.0 1.13.0 1.14.0 1.14.1 1.5.10 1.5.11 1.5.12 1.5.13 1.5.14 1.5.15 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.10 1.6.11 1.6.12 1.6.13 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.16 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.2.0 2.2.1 2.2.2 2.2.3 2.2.3-beta1 2.3.0 2.3.1 2.3.2 2.3.3 3.0.0 3.0.0-beta1 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.1.0 3.1.1 3.1.2 3.1.3 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4
presto-player / src / admin / dashboard / utils / dateUtils.js
presto-player / src / admin / dashboard / utils Last commit date
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