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 / hooks / useEngagementChartData.js
presto-player / src / admin / dashboard / hooks Last commit date
test 1 week ago useCompleteOnboarding.js 1 week ago useDateRangePicker.js 1 month ago useEmail.ts 1 month ago useEngagementChartData.js 1 month ago useLicenseSettings.js 1 month ago useLink.js 1 month ago useMediaDetail.js 1 month ago useMediaLibrary.js 1 month ago useMediaList.ts 1 month ago usePerformanceSettings.js 1 month ago useRegisterActivePage.js 1 month ago useSettingOption.js 1 month ago useSimpleSettingsPage.js 1 month ago useTopPerforming.js 1 month ago useTopVideosPaginated.js 1 month ago useUpgradeCTA.js 1 month ago useUserDetail.js 1 month ago
useEngagementChartData.js
362 lines
1 import { useState, useMemo, useEffect, useRef, useCallback } from "@wordpress/element";
2 import { __ } from "@wordpress/i18n";
3 import {
4 formatChartData,
5 padDailyPoints,
6 format,
7 getAllTimeRange,
8 getLastNDays,
9 DEFAULT_ANALYTICS_DAYS,
10 fetchViews,
11 fetchWatchTime,
12 toAnalyticsDate,
13 } from "../utils";
14
15 // Watch time stays in seconds for the chart so low values (e.g. 6s) plot
16 // above the x-axis. The Y-axis and tooltip handle unit formatting.
17 const formatData = (data, range) =>
18 padDailyPoints(formatChartData(data, "total", false, format), range, format);
19
20 const useEngagementChartData = ({ showAllTimeSummary = false }) => {
21 // Analytics endpoints are Pro-only. For free users, skip the fetch and let
22 // the chart render its built-in "No Stats Available" empty state — rather
23 // than firing doomed requests that surface as a red error banner.
24 const isPremium = !! window.prestoPlayer?.isPremium;
25
26 const [activeChartTab, setActiveChartTab] = useState("views");
27 const [selectedDates, setSelectedDates] = useState({ from: null, to: null });
28 const [mountData, setMountData] = useState(null);
29 const [isInitialLoading, setIsInitialLoading] = useState(isPremium);
30 const [filteredResult, setFilteredResult] = useState(null);
31 const [isFetching, setIsFetching] = useState(false);
32 // Errors are tracked per-metric so a watch-time failure doesn't paint a
33 // red banner on the views tab (and vice versa). `filterError` is only
34 // relevant while a `filteredResult` is in play; it resets on tab switch
35 // because the in-flight fetch was for the prior tab.
36 const [mountErrors, setMountErrors] = useState({
37 views: null,
38 watchTime: null,
39 });
40 const [filterError, setFilterError] = useState(null);
41
42 const mountAbortRef = useRef(null);
43 const fetchAbortRef = useRef(null);
44 const defaultDateRange = useMemo(
45 () => getLastNDays(DEFAULT_ANALYTICS_DAYS),
46 []
47 );
48
49 // --- Mount-level fetch (default last-N-days + optional all-time summary) ---
50 useEffect(() => {
51 if (!isPremium) {
52 return;
53 }
54
55 mountAbortRef.current = new AbortController();
56 const { signal } = mountAbortRef.current;
57
58 const fetchInitialData = async () => {
59 try {
60 setIsInitialLoading(true);
61 setMountErrors({ views: null, watchTime: null });
62
63 const { from: startDate, to: endDate } = getLastNDays(
64 DEFAULT_ANALYTICS_DAYS
65 );
66
67 const start = toAnalyticsDate(startDate);
68 const end = toAnalyticsDate(endDate);
69
70 // When the consumer asks for an all-time summary (Dashboard's two
71 // "All Time …" cards), fetch the all-time totals in parallel —
72 // the chart itself runs on the default last-N-days window above,
73 // so the summary needs its own pair of requests.
74 const requests = [
75 fetchViews(start, end, { signal }),
76 fetchWatchTime(start, end, { signal }),
77 ];
78 if (showAllTimeSummary) {
79 const all = getAllTimeRange();
80 const allStart = toAnalyticsDate(all.from);
81 const allEnd = toAnalyticsDate(all.to);
82 requests.push(fetchViews(allStart, allEnd, { signal }));
83 requests.push(fetchWatchTime(allStart, allEnd, { signal }));
84 }
85
86 const results = await Promise.allSettled(requests);
87
88 const views =
89 results[0].status === "fulfilled"
90 ? results[0].value
91 : { total: 0, chartData: [] };
92 const watchTime =
93 results[1].status === "fulfilled"
94 ? results[1].value
95 : { total: 0, chartData: [], average: 0 };
96
97 let allTimeViewsTotal = null;
98 let allTimeWatchTimeTotal = null;
99 if (showAllTimeSummary) {
100 if (results[2]?.status === "fulfilled") {
101 allTimeViewsTotal = results[2].value.total;
102 } else if (results[2]?.status === "rejected") {
103 // Aborts are noise; everything else is a genuine failure that
104 // would otherwise leave the "All Time Views" card silently blank.
105 if (results[2].reason?.name !== "AbortError") {
106 console.warn(
107 "Failed to load all-time views summary:",
108 results[2].reason
109 );
110 }
111 }
112 if (results[3]?.status === "fulfilled") {
113 allTimeWatchTimeTotal = results[3].value.total;
114 } else if (results[3]?.status === "rejected") {
115 if (results[3].reason?.name !== "AbortError") {
116 console.warn(
117 "Failed to load all-time watch-time summary:",
118 results[3].reason
119 );
120 }
121 }
122 }
123
124 setMountData({
125 totalViews: views.total,
126 viewsChartData: views.chartData,
127 totalWatchTime: watchTime.total,
128 watchTimeChartData: watchTime.chartData,
129 range: { from: startDate, to: endDate },
130 allTimeViews: allTimeViewsTotal,
131 allTimeWatchTime: allTimeWatchTimeTotal,
132 });
133
134 setMountErrors({
135 views:
136 results[0].status === "rejected"
137 ? __(
138 "Couldn't load views. Try refreshing the page.",
139 "presto-player"
140 )
141 : null,
142 watchTime:
143 results[1].status === "rejected"
144 ? __(
145 "Couldn't load watch time. Try refreshing the page.",
146 "presto-player"
147 )
148 : null,
149 });
150 } catch (err) {
151 if (err.name === "AbortError") return;
152 console.error("Error fetching engagement data:", err);
153 setMountData({
154 totalViews: 0,
155 viewsChartData: [],
156 totalWatchTime: 0,
157 watchTimeChartData: [],
158 allTimeViews: null,
159 allTimeWatchTime: null,
160 });
161 setMountErrors({
162 views: __(
163 "Couldn't load views. Try refreshing the page.",
164 "presto-player"
165 ),
166 watchTime: __(
167 "Couldn't load watch time. Try refreshing the page.",
168 "presto-player"
169 ),
170 });
171 } finally {
172 setIsInitialLoading(false);
173 }
174 };
175
176 fetchInitialData();
177
178 return () => {
179 mountAbortRef.current?.abort();
180 };
181 }, [showAllTimeSummary, isPremium]);
182
183 // --- Derived values ---
184 const isWatchTime = activeChartTab === "watch-time";
185
186 const dataToShow = useMemo(() => {
187 if (filteredResult?.chartData) return filteredResult.chartData;
188 if (!mountData) return [];
189
190 const source = isWatchTime
191 ? mountData.watchTimeChartData
192 : mountData.viewsChartData;
193 if (!source.length) return [];
194
195 return formatData(source, mountData.range);
196 }, [isWatchTime, mountData, filteredResult]);
197
198 const currentTotal = useMemo(() => {
199 if (filteredResult?.total != null) return filteredResult.total;
200 if (!mountData) return 0;
201 return isWatchTime
202 ? (mountData.totalWatchTime || 0)
203 : (mountData.totalViews || 0);
204 }, [isWatchTime, mountData, filteredResult]);
205
206 // Pick the error appropriate for what the user is currently looking at:
207 // - while a date filter is active, the in-flight metric's filter error
208 // - otherwise, the active tab's mount-fetch error
209 // This stops a failure on one metric from painting a banner on the
210 // other tab where the data is fine.
211 const activeError = useMemo(() => {
212 if (filteredResult !== null) return filterError;
213 return isWatchTime ? mountErrors.watchTime : mountErrors.views;
214 }, [filteredResult, filterError, isWatchTime, mountErrors]);
215
216 // --- Date-filter fetch ---
217 const fetchChartData = useCallback(async (dates, chartType) => {
218 if (!isPremium) {
219 return;
220 }
221 if (!dates.from) {
222 return;
223 }
224
225 if (fetchAbortRef.current) {
226 fetchAbortRef.current.abort();
227 }
228
229 fetchAbortRef.current = new AbortController();
230 const { signal } = fetchAbortRef.current;
231
232 const start = toAnalyticsDate(dates.from);
233 const end = toAnalyticsDate(dates.to || dates.from);
234 const isWT = chartType === "watch-time";
235
236 setIsFetching(true);
237
238 try {
239 setFilterError(null);
240 const result = isWT
241 ? await fetchWatchTime(start, end, { signal })
242 : await fetchViews(start, end, { signal });
243
244 const range = { from: dates.from, to: dates.to || dates.from };
245 if (result.chartData?.length) {
246 setFilteredResult({
247 chartData: formatData(result.chartData, range),
248 total: result.total,
249 });
250 } else {
251 setFilteredResult({
252 chartData: formatData([], range),
253 total: 0,
254 });
255 }
256 } catch (err) {
257 if (err.name !== "AbortError") {
258 console.error("Error fetching chart data:", err);
259 setFilterError(
260 __("Failed to load chart data. Please try again.", "presto-player")
261 );
262 setFilteredResult({ chartData: [], total: 0 });
263 }
264 } finally {
265 setIsFetching(false);
266 }
267 }, [isPremium]);
268
269 useEffect(() => {
270 if (selectedDates.from) {
271 fetchChartData(selectedDates, activeChartTab);
272 }
273
274 return () => {
275 if (fetchAbortRef.current) {
276 fetchAbortRef.current.abort();
277 }
278 };
279 }, [selectedDates.from, selectedDates.to, activeChartTab, fetchChartData]);
280
281 // --- Callbacks ---
282 const handleClearFilters = useCallback(() => {
283 setSelectedDates({ from: null, to: null });
284 setFilteredResult(null);
285 setFilterError(null);
286 }, []);
287
288 const handleTabChange = useCallback(({ value }) => {
289 setActiveChartTab(value.slug);
290 // Drop the prior tab's filtered result + filter error; the effect will
291 // refetch for the new tab if a date range is still selected.
292 setFilteredResult(null);
293 setFilterError(null);
294 }, []);
295
296 const handleDateApply = useCallback((dates) => {
297 const { from, to } = dates;
298
299 if (from && to) {
300 const fromDate = new Date(from);
301 const toDate = new Date(to);
302 setSelectedDates(fromDate > toDate ? { from: to, to: from } : dates);
303 } else if (from && !to) {
304 setSelectedDates({ from, to: from });
305 } else {
306 setSelectedDates({ from: null, to: null });
307 }
308 }, []);
309
310 // --- Context-ready callbacks (match what consumers expect) ---
311 const setActiveTab = useCallback(
312 (slug) => handleTabChange({ value: { slug } }),
313 [handleTabChange]
314 );
315
316 const setSelectedDatesForContext = useCallback(
317 (dates) => {
318 if (dates === null) {
319 handleClearFilters();
320 } else {
321 handleDateApply(dates);
322 }
323 },
324 [handleClearFilters, handleDateApply]
325 );
326
327 // --- Return context-ready value ---
328 return useMemo(
329 () => ({
330 activeTab: activeChartTab,
331 setActiveTab,
332 chartData: dataToShow,
333 total: currentTotal,
334 allTimeViews: showAllTimeSummary ? (mountData?.allTimeViews ?? null) : null,
335 allTimeWatchTime: showAllTimeSummary ? (mountData?.allTimeWatchTime ?? null) : null,
336 isWatchTime,
337 selectedDates,
338 setSelectedDates: setSelectedDatesForContext,
339 defaultDateRange,
340 isLoading: isInitialLoading || isFetching,
341 error: activeError,
342 }),
343 [
344 activeChartTab,
345 setActiveTab,
346 dataToShow,
347 currentTotal,
348 showAllTimeSummary,
349 mountData,
350 isWatchTime,
351 selectedDates,
352 setSelectedDatesForContext,
353 defaultDateRange,
354 isInitialLoading,
355 isFetching,
356 activeError,
357 ]
358 );
359 };
360
361 export default useEngagementChartData;
362