PluginProbe ʕ •ᴥ•ʔ
Presto Player / trunk
Presto Player vtrunk
4.3.1 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 / pages / UserAnalyticsDetail.js
presto-player / src / admin / dashboard / pages Last commit date
learn 1 month ago settings 1 month ago Analytics.js 1 month ago AnalyticsDetail.js 1 month ago Dashboard.js 1 month ago Emails.js 1 month ago Learn.js 1 month ago MediaHub.js 3 weeks ago Onboarding.js 3 weeks ago Settings.js 1 month ago UserAnalyticsDetail.js 1 month ago
UserAnalyticsDetail.js
244 lines
1 const { __ } = wp.i18n;
2 import { useMemo } from "@wordpress/element";
3 import {
4 Avatar,
5 Container,
6 Button,
7 Text,
8 Input,
9 DatePicker,
10 } from "@bsf/force-ui";
11 import { ArrowLeft, Calendar, X } from "lucide-react";
12 import { useHistory, useLocation } from "../router/router";
13 import useUserDetail from "../hooks/useUserDetail";
14 import useDateRangePicker from "../hooks/useDateRangePicker";
15 import {
16 getRangeLabel,
17 getLastNDays,
18 getAnalyticsPresets,
19 DEFAULT_ANALYTICS_DAYS,
20 } from "../utils";
21 import StatCard from "../components/StatCard";
22 import TopMedia from "../components/TopMedia";
23 import UserAnalyticsDetailSkeleton from "../components/Skeletons/UserAnalyticsDetailSkeleton";
24
25 /**
26 * User Analytics Detail Page
27 *
28 * Reached by clicking a user row in the Top Users table on the Analytics page.
29 * URL pattern: ?tab=Analytics&detail=user&id={userId}
30 *
31 * Layout:
32 * - Header: back button, avatar, clickable username (links to WP profile), email, date range picker
33 * - 3 stat cards: Total Views, Average Watch Time, Total Watch Time
34 * - Top Media table (reused TopMedia component, filtered by user_id)
35 */
36 const UserAnalyticsDetail = () => {
37 const history = useHistory();
38 const location = useLocation();
39 const userId = location.params?.id;
40
41 const {
42 user,
43 stats,
44 topMedia,
45 mediaPage,
46 setMediaPage,
47 mediaPagination,
48 mediaPerPage,
49 isLoading,
50 error,
51 refetch,
52 } = useUserDetail(userId);
53
54 const {
55 selectedDates,
56 isDatePickerOpen,
57 setIsDatePickerOpen,
58 datePickerRef,
59 handleDateApply,
60 handleDateCancel,
61 handleClearFilters,
62 } = useDateRangePicker(refetch);
63
64 const presets = useMemo(() => getAnalyticsPresets(), []);
65 const defaultDateRange = useMemo(
66 () => getLastNDays(DEFAULT_ANALYTICS_DAYS),
67 []
68 );
69
70 const fromTab = location.params?.from;
71 const handleBack = () => {
72 if (fromTab) {
73 history.replace({ tab: fromTab, detail: null, id: null, from: null });
74 } else {
75 history.replace({ tab: "Analytics", detail: null, id: null });
76 }
77 };
78
79 // Loading state — show skeleton until all data (user + stats + topMedia) is ready
80 if (isLoading) {
81 return <UserAnalyticsDetailSkeleton />;
82 }
83
84 // Error state
85 if (error) {
86 return (
87 <Container
88 className="p-8 bg-background-secondary"
89 direction="column"
90 gap="md"
91 >
92 <Button
93 variant="ghost"
94 size="xs"
95 icon={<ArrowLeft className="size-5" />}
96 onClick={handleBack}
97 />
98 <Container
99 align="center"
100 justify="center"
101 className="p-8 bg-background-primary border border-solid border-border-subtle rounded-lg"
102 >
103 <Text size={14} weight={400} color="secondary">
104 {error}
105 </Text>
106 </Container>
107 </Container>
108 );
109 }
110
111 return (
112 <Container
113 className="p-8 bg-background-secondary"
114 direction="column"
115 gap="md"
116 >
117 {/* Header: Back + Avatar + User Info + Date Picker */}
118 <Container align="center" gap="sm">
119 <Button
120 variant="ghost"
121 size="xs"
122 icon={<ArrowLeft className="size-5" />}
123 onClick={handleBack}
124 className="shrink-0"
125 />
126
127 <Avatar
128 size="md"
129 variant="primary"
130 src={user?.avatarUrl}
131 alt={user?.name}
132 className="shrink-0"
133 >
134 {(user?.name || "?").charAt(0).toUpperCase()}
135 </Avatar>
136
137 <Container direction="column" gap="none" className="flex-1 min-w-0">
138 <a
139 href={user?.profileUrl}
140 target="_blank"
141 rel="noopener noreferrer"
142 className="no-underline hover:underline w-fit"
143 >
144 <Text
145 size={20}
146 weight={600}
147 color="primary"
148 className="truncate"
149 >
150 {user?.name || __("User Detail", "presto-player")}
151 </Text>
152 </a>
153 {user?.email && (
154 <Text size={13} weight={400} color="tertiary">
155 {user.email}
156 </Text>
157 )}
158 </Container>
159
160 {/* Date Range Picker */}
161 <Container align="center" gap="xs" className="shrink-0">
162 {(selectedDates.from || selectedDates.to) && (
163 <Button
164 variant="link"
165 size="xs"
166 destructive
167 icon={<X />}
168 onClick={handleClearFilters}
169 aria-label={__("Clear Filters", "presto-player")}
170 >
171 {__("Clear Filters", "presto-player")}
172 </Button>
173 )}
174 <div className="relative" ref={datePickerRef}>
175 <Input
176 type="text"
177 size="sm"
178 value={getRangeLabel(
179 selectedDates?.from && selectedDates?.to
180 ? selectedDates
181 : defaultDateRange
182 )}
183 suffix={<Calendar className="text-icon-secondary" />}
184 onClick={() => setIsDatePickerOpen((prev) => !prev)}
185 className="w-60 cursor-pointer"
186 readOnly
187 aria-label={__("Select Date Range", "presto-player")}
188 />
189 {isDatePickerOpen && (
190 <Container className="absolute z-10 mt-2 rounded-lg shadow-lg right-0 bg-background-primary">
191 <DatePicker
192 applyButtonText={__("Apply", "presto-player")}
193 cancelButtonText={__("Cancel", "presto-player")}
194 selectionType="range"
195 showOutsideDays={false}
196 variant="presets"
197 presets={presets}
198 onApply={handleDateApply}
199 onCancel={handleDateCancel}
200 selected={
201 selectedDates?.from && selectedDates?.to
202 ? selectedDates
203 : defaultDateRange
204 }
205 disabled={{ after: new Date() }}
206 />
207 </Container>
208 )}
209 </div>
210 </Container>
211 </Container>
212
213 {/* Stat Cards Row */}
214 <Container containerType="grid" gap="md" className="grid-cols-3">
215 <StatCard
216 label={__("Views", "presto-player")}
217 value={String(stats.totalViews)}
218 />
219 <StatCard
220 label={__("Average Watch Time", "presto-player")}
221 value={stats.avgWatchTime}
222 />
223 <StatCard
224 label={__("Total Watch Time", "presto-player")}
225 value={stats.totalWatchTime}
226 />
227 </Container>
228
229 {/* Top Media Table (reused component) */}
230 <TopMedia
231 media={topMedia}
232 fromUserId={userId}
233 currentPage={mediaPage}
234 totalItems={mediaPagination.totalItems}
235 totalPages={mediaPagination.totalPages}
236 perPage={mediaPerPage}
237 onPageChange={setMediaPage}
238 />
239 </Container>
240 );
241 };
242
243 export default UserAnalyticsDetail;
244