PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.6
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.6
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / assets / js / widget-site-views.js

widget-site-views.js in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.6, at assets/js/widget-site-views.js

276 lines 9.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function() {
2 "use strict";
3 const NONCE_HEADER = "X-WP-Nonce";
4 function injectRestNonce(input, init) {
5 const nonce = readRestNonce();
6 if (!nonce) {
7 return init;
8 }
9 const url = resolveUrl(input);
10 if (!url || !isSameOriginRestUrl(url)) {
11 return init;
12 }
13 const baseHeaders = init?.headers ?? (typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
14 const headers = new Headers(baseHeaders ?? {});
15 if (headers.has(NONCE_HEADER)) {
16 return init;
17 }
18 headers.set(NONCE_HEADER, nonce);
19 return { ...init ?? {}, headers };
20 }
21 function readRestNonce() {
22 if (typeof window === "undefined") {
23 return void 0;
24 }
25 const cfg = window.desktopModeConfig;
26 const value = cfg?.restNonce;
27 return typeof value === "string" && value.length > 0 ? value : void 0;
28 }
29 function resolveUrl(input) {
30 try {
31 const base = typeof window !== "undefined" && window.location ? window.location.href : void 0;
32 if (typeof input === "string") {
33 return new URL(input, base);
34 }
35 if (input instanceof URL) {
36 return input;
37 }
38 if (typeof Request !== "undefined" && input instanceof Request) {
39 return new URL(input.url, base);
40 }
41 return null;
42 } catch {
43 return null;
44 }
45 }
46 function isSameOriginRestUrl(url) {
47 if (typeof window === "undefined" || !window.location || url.origin !== window.location.origin) {
48 return false;
49 }
50 if (url.pathname.includes("/wp-json/")) {
51 return true;
52 }
53 if (url.searchParams.has("rest_route")) {
54 return true;
55 }
56 return false;
57 }
58 function trackedFetch(input, init, opts = {}) {
59 const fn = window.wp?.desktop?.fetch;
60 if (typeof fn === "function") {
61 return fn(input, init, opts);
62 }
63 const finalInit = injectRestNonce(input, init);
64 return fetch(input, finalInit);
65 }
66 const WIDGET_ID = "desktop-mode/site-views";
67 const REFRESH_MS = 10 * 6e4;
68 function shortDay(iso) {
69 return (/* @__PURE__ */ new Date(iso + "T12:00:00")).toLocaleString(void 0, { weekday: "short" }).slice(0, 3);
70 }
71 function apiRoot() {
72 const s = window.wpApiSettings ?? {};
73 return (s.root ?? "/wp-json/").replace(/\/$/, "");
74 }
75 async function tryJetpack() {
76 try {
77 const res = await trackedFetch(
78 apiRoot() + "/jetpack/v4/stats/visits?unit=day&quantity=14",
79 { credentials: "same-origin" },
80 { source: "desktop-mode/site-views-jetpack", silent: true }
81 );
82 if (!res.ok) {
83 return null;
84 }
85 const data = await res.json();
86 if (!Array.isArray(data?.data)) {
87 return null;
88 }
89 return data.data.map(([ts, views]) => ({
90 date: new Date(ts * 1e3).toISOString().slice(0, 10),
91 views: views || 0
92 }));
93 } catch {
94 return null;
95 }
96 }
97 async function tryMeta() {
98 try {
99 const res = await trackedFetch(
100 apiRoot() + "/desktop-mode/v1/site-views-meta",
101 { credentials: "same-origin" },
102 { source: "desktop-mode/site-views-meta", silent: true }
103 );
104 if (!res.ok) {
105 return null;
106 }
107 const data = await res.json();
108 if (!data?.has_data) {
109 return null;
110 }
111 return data.days ?? null;
112 } catch {
113 return null;
114 }
115 }
116 async function fetchViewData() {
117 const jetpack = await tryJetpack();
118 if (jetpack?.length) {
119 return { source: "jetpack", days: jetpack };
120 }
121 const meta = await tryMeta();
122 if (meta?.length) {
123 return { source: "meta", days: meta };
124 }
125 return { source: "none", days: [] };
126 }
127 function buildSparkPath(values, W, H, pad) {
128 if (values.length < 2) {
129 return null;
130 }
131 const max = Math.max(1, ...values);
132 const pts = values.map((v, i) => [
133 pad + (W - pad * 2) / (values.length - 1) * i,
134 H - pad - v / max * (H - pad * 2)
135 ]);
136 let d = `M ${pts[0][0]} ${pts[0][1]}`;
137 for (let i = 1; i < pts.length; i++) {
138 const cpx = (pts[i - 1][0] + pts[i][0]) / 2;
139 d += ` C ${cpx} ${pts[i - 1][1]}, ${cpx} ${pts[i][1]}, ${pts[i][0]} ${pts[i][1]}`;
140 }
141 const area = d + ` L ${pts[pts.length - 1][0]} ${H} L ${pts[0][0]} ${H} Z`;
142 return { line: d, area, pts };
143 }
144 function renderUI(container, result, error) {
145 container.innerHTML = "";
146 const header = document.createElement("div");
147 header.className = "dm-views__header";
148 const title = document.createElement("span");
149 title.className = "dm-views__title";
150 title.textContent = "Site Views";
151 header.appendChild(title);
152 container.appendChild(header);
153 if (error) {
154 const e = document.createElement("div");
155 e.className = "dm-views__error";
156 e.textContent = "Could not load view data.";
157 container.appendChild(e);
158 return;
159 }
160 if (!result || result.source === "none" || result.days.length === 0) {
161 const ns = document.createElement("div");
162 ns.className = "dm-views__no-source";
163 const p = document.createElement("p");
164 p.textContent = "No stats source found. Activate Jetpack Stats or a post-views plugin that writes ";
165 const code = document.createElement("code");
166 code.textContent = "_post_views_YYYY-MM-DD";
167 p.appendChild(code);
168 p.appendChild(document.createTextNode(" meta keys."));
169 ns.appendChild(p);
170 container.appendChild(ns);
171 return;
172 }
173 const thisWeek = result.days.slice(-7);
174 const prevWeek = result.days.slice(-14, -7);
175 const thisTotal = thisWeek.reduce((s, d) => s + d.views, 0);
176 const prevTotal = prevWeek.reduce((s, d) => s + d.views, 0);
177 const kpi = document.createElement("div");
178 kpi.className = "dm-views__kpi";
179 const totalEl = document.createElement("span");
180 totalEl.className = "dm-views__total";
181 totalEl.textContent = thisTotal.toLocaleString();
182 const deltaEl = document.createElement("span");
183 deltaEl.className = "dm-views__delta";
184 if (prevTotal === 0) {
185 deltaEl.classList.add("dm-views__delta--flat");
186 deltaEl.textContent = "—";
187 } else {
188 const pct = Math.round((thisTotal - prevTotal) / prevTotal * 100);
189 if (pct > 0) {
190 deltaEl.classList.add("dm-views__delta--up");
191 deltaEl.textContent = "↑ " + pct + "%";
192 } else if (pct < 0) {
193 deltaEl.classList.add("dm-views__delta--down");
194 deltaEl.textContent = "↓ " + Math.abs(pct) + "%";
195 } else {
196 deltaEl.classList.add("dm-views__delta--flat");
197 deltaEl.textContent = "→ 0%";
198 }
199 }
200 const label = document.createElement("span");
201 label.className = "dm-views__label";
202 label.textContent = "views this week";
203 kpi.appendChild(totalEl);
204 kpi.appendChild(deltaEl);
205 kpi.appendChild(label);
206 container.appendChild(kpi);
207 const svgNS = "http://www.w3.org/2000/svg";
208 const sparkWrap = document.createElement("div");
209 sparkWrap.className = "dm-views__spark";
210 const svg = document.createElementNS(svgNS, "svg");
211 svg.setAttribute("viewBox", "0 0 300 80");
212 svg.setAttribute("preserveAspectRatio", "none");
213 const paths = buildSparkPath(thisWeek.map((d) => d.views), 300, 80, 4);
214 if (paths) {
215 const area = document.createElementNS(svgNS, "path");
216 area.setAttribute("d", paths.area);
217 area.setAttribute("fill", "rgba(59,130,246,0.12)");
218 svg.appendChild(area);
219 const line = document.createElementNS(svgNS, "path");
220 line.setAttribute("d", paths.line);
221 line.setAttribute("fill", "none");
222 line.setAttribute("stroke", "#3b82f6");
223 line.setAttribute("stroke-width", "2");
224 line.setAttribute("stroke-linecap", "round");
225 svg.appendChild(line);
226 const last = paths.pts[paths.pts.length - 1];
227 const dot = document.createElementNS(svgNS, "circle");
228 dot.setAttribute("cx", String(last[0]));
229 dot.setAttribute("cy", String(last[1]));
230 dot.setAttribute("r", "3");
231 dot.setAttribute("fill", "#3b82f6");
232 svg.appendChild(dot);
233 }
234 sparkWrap.appendChild(svg);
235 container.appendChild(sparkWrap);
236 const daysEl = document.createElement("div");
237 daysEl.className = "dm-views__days";
238 thisWeek.forEach((d) => {
239 const span = document.createElement("span");
240 span.textContent = shortDay(d.date);
241 daysEl.appendChild(span);
242 });
243 container.appendChild(daysEl);
244 }
245 const mount = async (container, _ctx) => {
246 let destroyed = false;
247 let intervalId = null;
248 const refresh = async () => {
249 if (destroyed) {
250 return;
251 }
252 try {
253 const result = await fetchViewData();
254 if (!destroyed) {
255 renderUI(container, result, false);
256 }
257 } catch {
258 if (!destroyed) {
259 renderUI(container, null, true);
260 }
261 }
262 };
263 await refresh();
264 intervalId = setInterval(refresh, REFRESH_MS);
265 return () => {
266 destroyed = true;
267 if (intervalId !== null) {
268 clearInterval(intervalId);
269 }
270 };
271 };
272 const w = window;
273 w.desktopModeWidgets = w.desktopModeWidgets ?? {};
274 w.desktopModeWidgets[WIDGET_ID] = mount;
275 })();
276