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-post-stats.js

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

303 lines 9.8 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/post-stats";
67 const REFRESH_MS = 5 * 6e4;
68 const MONTHS_BACK = 6;
69 const COLORS = {
70 published: "#3b82f6",
71 pending: "#fbbf24",
72 draft: "#a5b4fc"
73 };
74 function monthKey(date) {
75 return date.getFullYear() + "-" + String(date.getMonth() + 1).padStart(2, "0");
76 }
77 function shortLabel(ym) {
78 const [, m] = ym.split("-");
79 return new Date(2e3, parseInt(m, 10) - 1, 1).toLocaleString(void 0, { month: "short" });
80 }
81 function buildBuckets() {
82 const now = /* @__PURE__ */ new Date();
83 return Array.from({ length: MONTHS_BACK }, (_, i) => {
84 const d = new Date(now.getFullYear(), now.getMonth() - (MONTHS_BACK - 1 - i), 1);
85 const ym = monthKey(d);
86 return { ym, label: shortLabel(ym), published: 0, pending: 0, draft: 0 };
87 });
88 }
89 async function fetchPosts() {
90 const root = window.wpApiSettings?.root ?? "/wp-json/";
91 const cutoff = /* @__PURE__ */ new Date();
92 cutoff.setMonth(cutoff.getMonth() - MONTHS_BACK);
93 cutoff.setDate(1);
94 const after = cutoff.toISOString();
95 const statuses = ["publish", "draft", "pending"];
96 const all = [];
97 for (const status of statuses) {
98 let page = 1;
99 let total = Infinity;
100 while (all.length < 200 && (page - 1) * 100 < total) {
101 const res = await trackedFetch(
102 root.replace(/\/$/, "") + `/wp/v2/posts?per_page=100&page=${page}&status=${status}&after=${encodeURIComponent(after)}&_fields=id,date,status`,
103 { credentials: "same-origin" },
104 { source: "desktop-mode/post-stats", silent: true }
105 );
106 if (!res.ok) {
107 break;
108 }
109 total = parseInt(res.headers.get("X-WP-Total") ?? "0", 10);
110 const posts = await res.json();
111 if (!Array.isArray(posts) || posts.length === 0) {
112 break;
113 }
114 all.push(...posts);
115 page++;
116 }
117 }
118 return all;
119 }
120 function safeRoundRect(ctx, rx, ry, rw, rh, rr) {
121 if (typeof ctx.roundRect === "function") {
122 ctx.roundRect(rx, ry, rw, rh, rr);
123 } else {
124 ctx.rect(rx, ry, rw, rh);
125 }
126 }
127 function drawChart(canvas, buckets) {
128 const dpr = window.devicePixelRatio || 1;
129 const rect = canvas.getBoundingClientRect();
130 if (rect.width === 0 || rect.height === 0) {
131 return;
132 }
133 canvas.width = Math.round(rect.width * dpr);
134 canvas.height = Math.round(rect.height * dpr);
135 const ctx = canvas.getContext("2d");
136 if (!ctx) {
137 return;
138 }
139 ctx.scale(dpr, dpr);
140 const W = rect.width;
141 const H = rect.height;
142 const PAD = { top: 18, right: 10, bottom: 22, left: 28 };
143 const chartW = W - PAD.left - PAD.right;
144 const chartH = H - PAD.top - PAD.bottom;
145 if (chartW <= 0 || chartH <= 0) {
146 return;
147 }
148 const maxVal = Math.max(1, ...buckets.map((b) => b.published + b.pending + b.draft));
149 const barGroupW = chartW / buckets.length;
150 const barPad = barGroupW * 0.2;
151 const barW = Math.max(1, barGroupW - barPad * 2);
152 ctx.strokeStyle = "rgba(0,0,0,0.07)";
153 ctx.lineWidth = 1;
154 for (let i = 0; i <= 3; i++) {
155 const y = PAD.top + chartH - chartH * i / 3;
156 ctx.beginPath();
157 ctx.moveTo(PAD.left, y);
158 ctx.lineTo(PAD.left + chartW, y);
159 ctx.stroke();
160 ctx.fillStyle = "rgba(0,0,0,0.35)";
161 ctx.font = "9px -apple-system, sans-serif";
162 ctx.textAlign = "right";
163 ctx.fillText(String(Math.round(maxVal * i / 3)), PAD.left - 4, y + 3);
164 }
165 for (let i = 0; i < buckets.length; i++) {
166 const b = buckets[i];
167 const x = PAD.left + barGroupW * i + barPad;
168 let yTop = PAD.top + chartH;
169 for (const seg of [
170 { value: b.published, color: COLORS.published },
171 { value: b.pending, color: COLORS.pending },
172 { value: b.draft, color: COLORS.draft }
173 ]) {
174 if (seg.value === 0) {
175 continue;
176 }
177 const segH = seg.value / maxVal * chartH;
178 yTop -= segH;
179 ctx.fillStyle = seg.color;
180 ctx.beginPath();
181 safeRoundRect(ctx, x, yTop, barW, segH, 2);
182 ctx.fill();
183 }
184 ctx.fillStyle = "rgba(0,0,0,0.5)";
185 ctx.font = "9px -apple-system, sans-serif";
186 ctx.textAlign = "center";
187 ctx.fillText(b.label, x + barW / 2, PAD.top + chartH + 13);
188 }
189 }
190 function renderHeader(container, total, error) {
191 const header = document.createElement("div");
192 header.className = "dm-poststats__header";
193 const title = document.createElement("span");
194 title.className = "dm-poststats__title";
195 title.textContent = "Post Stats";
196 const totalEl = document.createElement("span");
197 totalEl.className = "dm-poststats__total";
198 totalEl.textContent = error ? "" : `${total} post${total !== 1 ? "s" : ""} in 6 mo`;
199 header.appendChild(title);
200 header.appendChild(totalEl);
201 container.appendChild(header);
202 }
203 function buildLegend(container) {
204 const legend = document.createElement("div");
205 legend.className = "dm-poststats__legend";
206 for (const [label, color] of [
207 ["Published", COLORS.published],
208 ["Pending", COLORS.pending],
209 ["Draft", COLORS.draft]
210 ]) {
211 const item = document.createElement("div");
212 item.className = "dm-poststats__legend-item";
213 const swatch = document.createElement("span");
214 swatch.className = "dm-poststats__legend-swatch";
215 swatch.style.background = color;
216 item.appendChild(swatch);
217 item.appendChild(document.createTextNode(label));
218 legend.appendChild(item);
219 }
220 container.appendChild(legend);
221 }
222 const mount = async (container, _ctx) => {
223 let destroyed = false;
224 let intervalId = null;
225 let ro = null;
226 const refresh = async () => {
227 if (destroyed) {
228 return;
229 }
230 try {
231 const posts = await fetchPosts();
232 const buckets = buildBuckets();
233 for (const post of posts) {
234 const ym = post.date ? post.date.slice(0, 7) : null;
235 const bucket = ym ? buckets.find((b) => b.ym === ym) : null;
236 if (!bucket) {
237 continue;
238 }
239 if (post.status === "publish") {
240 bucket.published++;
241 } else if (post.status === "pending") {
242 bucket.pending++;
243 } else if (post.status === "draft") {
244 bucket.draft++;
245 }
246 }
247 const total = posts.length;
248 if (destroyed) {
249 return;
250 }
251 container.innerHTML = "";
252 renderHeader(container, total, false);
253 if (total === 0) {
254 const empty = document.createElement("div");
255 empty.className = "dm-poststats__empty";
256 empty.textContent = "No posts in the last 6 months.";
257 container.appendChild(empty);
258 return;
259 }
260 const wrap = document.createElement("div");
261 wrap.className = "dm-poststats__canvas-wrap";
262 const canvas = document.createElement("canvas");
263 canvas.className = "dm-poststats__canvas";
264 wrap.appendChild(canvas);
265 container.appendChild(wrap);
266 buildLegend(container);
267 ro?.disconnect();
268 ro = new ResizeObserver((entries) => {
269 if (destroyed) {
270 return;
271 }
272 const entry = entries[0];
273 if (entry && entry.contentRect.width > 0 && entry.contentRect.height > 0) {
274 drawChart(canvas, buckets);
275 }
276 });
277 ro.observe(wrap);
278 } catch {
279 if (!destroyed) {
280 container.innerHTML = "";
281 renderHeader(container, 0, true);
282 const errEl = document.createElement("div");
283 errEl.className = "dm-poststats__error";
284 errEl.textContent = "Could not load post data.";
285 container.appendChild(errEl);
286 }
287 }
288 };
289 await refresh();
290 intervalId = setInterval(refresh, REFRESH_MS);
291 return () => {
292 destroyed = true;
293 if (intervalId !== null) {
294 clearInterval(intervalId);
295 }
296 ro?.disconnect();
297 };
298 };
299 const w = window;
300 w.desktopModeWidgets = w.desktopModeWidgets ?? {};
301 w.desktopModeWidgets[WIDGET_ID] = mount;
302 })();
303