easy-starter 1.2.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +32 -80
  2. package/bin/index.js +219 -44
  3. package/package.json +20 -11
  4. package/template/.cursorrules +16 -9
  5. package/template/README.md +81 -24
  6. package/template/env +0 -1
  7. package/template/gitignore +1 -1
  8. package/template/index.html +15 -0
  9. package/template/package.json +8 -9
  10. package/template/public/images/icon-16.png +0 -0
  11. package/template/public/images/icon-180.png +0 -0
  12. package/template/public/images/icon-192.png +0 -0
  13. package/template/public/images/icon-32.png +0 -0
  14. package/template/public/images/icon-512.png +0 -0
  15. package/template/{src → public}/site.webmanifest +6 -12
  16. package/template/server/index.tsx +249 -53
  17. package/template/server/mail.tsx +120 -0
  18. package/template/src/App.tsx +33 -7
  19. package/template/src/Router.tsx +11 -2
  20. package/template/src/index.tsx +28 -1
  21. package/template/src/pages/Index.tsx +45 -6
  22. package/template/src/pages/ResetPassword.tsx +126 -0
  23. package/template/src/pages/admin/Admin.tsx +360 -85
  24. package/template/src/pages/admin/AnalyticsTab.tsx +71 -0
  25. package/template/src/pages/admin/ErrorsTab.tsx +46 -0
  26. package/template/src/pages/admin/UsersTab.tsx +61 -0
  27. package/template/src/services/api.ts +5 -7
  28. package/template/src/services/common.ts +23 -0
  29. package/template/src/styles.css +460 -0
  30. package/template/tsconfig.json +2 -1
  31. package/template/types.ts +44 -3
  32. package/template/vite.config.ts +24 -0
  33. package/template/.parcelrc +0 -6
  34. package/template/src/images/icon.png +0 -0
  35. package/template/src/index.html +0 -16
  36. package/template/src/styles.scss +0 -292
@@ -1,20 +1,218 @@
1
1
  import React, { useEffect, useState } from "react";
2
- import { useGroup, GroupTable, getGroupedCount } from "easy-analytics/react";
3
2
  import { Link } from "easy-page-router/react";
4
3
 
5
4
  import { useHead } from "../../services/common";
6
5
  import { api, handleError } from "../../services/api";
7
6
 
8
- export default function Admin() {
9
- useHead({ title: "Analytics Dashboard", description: "View statistics and analytics for your application." });
7
+ // --- ANALYTICS START ---
8
+ import AnalyticsTab from "./AnalyticsTab";
9
+ // --- ANALYTICS END ---
10
10
 
11
- const [password, setPassword] = useState("");
12
- const [loggedIn, setLoggedIn] = useState(false);
13
- const [error, setError] = useState("");
11
+ // --- ADMIN_ERRORS START ---
12
+ import ErrorsTab from "./ErrorsTab";
13
+ // --- ADMIN_ERRORS END ---
14
+
15
+ // --- ADMIN_USERS START ---
16
+ import UsersTab from "./UsersTab";
17
+ // --- ADMIN_USERS END ---
18
+
19
+ // --- AUTH START ---
20
+ import { useUser } from "easy-user-auth/react";
21
+ import { UserProfile } from "../../../types";
22
+ // --- AUTH END ---
23
+
24
+ type AdminProps = {
25
+ path: string[];
26
+ };
27
+
28
+ export default function Admin({ path }: AdminProps) {
29
+ useHead({ title: "Admin Dashboard", description: "Admin section for managing the application." });
14
30
 
15
31
  const [month, setMonth] = useState(new Date().toISOString().slice(0, 7));
16
32
  const [records, setRecords] = useState([]);
17
33
  const [loading, setLoading] = useState(false);
34
+ const [error, setError] = useState("");
35
+
36
+ const defaultTab =
37
+ // --- ANALYTICS START ---
38
+ "analytics"
39
+ // --- ANALYTICS END ---
40
+ // --- NO_ANALYTICS START ---
41
+ // --- ADMIN_ERRORS START ---
42
+ "errors"
43
+ // --- ADMIN_ERRORS END ---
44
+ // --- NO_ADMIN_ERRORS START ---
45
+ // --- ADMIN_USERS START ---
46
+ "users"
47
+ // --- ADMIN_USERS END ---
48
+ // --- NO_ADMIN_USERS START ---
49
+ "dashboard"
50
+ // --- NO_ADMIN_USERS END ---
51
+ // --- NO_ADMIN_ERRORS END ---
52
+ // --- NO_ANALYTICS END ---
53
+ ;
54
+
55
+ const activeTab = path[1] || defaultTab;
56
+
57
+ // --- ADMIN_ERRORS START ---
58
+ const [errorsList, setErrorsList] = useState<any[]>([]);
59
+ const [loadingErrors, setLoadingErrors] = useState(false);
60
+ const [errorLogsError, setErrorLogsError] = useState("");
61
+ // --- ADMIN_ERRORS END ---
62
+
63
+ // --- ADMIN_USERS START ---
64
+ const [usersList, setUsersList] = useState<any[]>([]);
65
+ const [loadingUsers, setLoadingUsers] = useState(false);
66
+ const [usersError, setUsersError] = useState("");
67
+ // --- ADMIN_USERS END ---
68
+
69
+ // --- ADMIN_WITH_AUTH START ---
70
+ const { isLoggedIn, user, loginUser, showUserDialog } = useUser<UserProfile>();
71
+
72
+ // --- ADMIN_USERS START ---
73
+ const fetchUsers = async () => {
74
+ setLoadingUsers(true);
75
+ setUsersError("");
76
+ try {
77
+ const [data, err] = await api.getUsers({});
78
+ if (data) {
79
+ setUsersList(data);
80
+ } else if (err) {
81
+ setUsersError(err.message || String(err));
82
+ }
83
+ } catch (e) {
84
+ setUsersError(String(e));
85
+ }
86
+ setLoadingUsers(false);
87
+ };
88
+
89
+ const handleRoleChange = async (userId: string, newRole: "admin" | "user") => {
90
+ try {
91
+ const [success, err] = await api.updateUserRole({ userId, role: newRole });
92
+ if (success) {
93
+ setUsersList(prev => prev.map(u => u._id === userId ? { ...u, role: newRole } : u));
94
+ } else if (err) {
95
+ alert("Failed to update role: " + err.message);
96
+ }
97
+ } catch (e) {
98
+ alert(String(e));
99
+ }
100
+ };
101
+ // --- ADMIN_USERS END ---
102
+
103
+ // --- ADMIN_ERRORS_WITH_AUTH START ---
104
+ const fetchErrors = async (m: string) => {
105
+ setLoadingErrors(true);
106
+ setErrorLogsError("");
107
+ try {
108
+ const [data, err] = await api.getEasyAnalyticsErrors({ month: m });
109
+ if (data) {
110
+ setErrorsList(data);
111
+ } else if (err) {
112
+ setErrorLogsError(err.message || String(err));
113
+ }
114
+ } catch (e) {
115
+ setErrorLogsError(String(e));
116
+ }
117
+ setLoadingErrors(false);
118
+ };
119
+ // --- ADMIN_ERRORS_WITH_AUTH END ---
120
+
121
+ useEffect(() => {
122
+ if (isLoggedIn && user?.role === "admin") {
123
+ if (activeTab === "analytics") {
124
+ fetchData(month);
125
+ }
126
+ // --- ADMIN_ERRORS START ---
127
+ if (activeTab === "errors") {
128
+ fetchErrors(month);
129
+ }
130
+ // --- ADMIN_ERRORS END ---
131
+ // --- ADMIN_USERS START ---
132
+ if (activeTab === "users") {
133
+ fetchUsers();
134
+ }
135
+ // --- ADMIN_USERS END ---
136
+ }
137
+ }, [isLoggedIn, user, month, activeTab]);
138
+
139
+ const fetchData = async (m: string) => {
140
+ setLoading(true);
141
+ setError("");
142
+ try {
143
+ // --- ANALYTICS START ---
144
+ const [data, err] = await api.getEasyAnalyticsData({ month: m });
145
+ if (data) {
146
+ setRecords(data as any);
147
+ } else if (err) {
148
+ setError(err.message || String(err));
149
+ }
150
+ // --- ANALYTICS END ---
151
+ } catch (e) {
152
+ setError(String(e));
153
+ handleError(e as Error);
154
+ }
155
+ setLoading(false);
156
+ };
157
+
158
+ const handleMonthChange = (move: number) => {
159
+ const newMonth = new Date(month);
160
+ newMonth.setMonth(newMonth.getMonth() + move);
161
+ const nextM = newMonth.toISOString().slice(0, 7);
162
+ setMonth(nextM);
163
+ };
164
+
165
+ if (!isLoggedIn || user?.role !== "admin") {
166
+ return (
167
+ <div className="admin-login" style={{ padding: 40, textAlign: 'center' }}>
168
+ {!isLoggedIn ? (
169
+ <>
170
+ <h2>Admin Login Required</h2>
171
+ <p>Please log in using an account with administrative permissions.</p>
172
+ <div style={{ display: 'flex', gap: 15, justifyContent: 'center', marginTop: 20 }}>
173
+ <button onClick={showUserDialog}>Log In</button>
174
+ <Link href="/">Back to website</Link>
175
+ </div>
176
+ <div style={{ marginTop: 30, fontSize: '0.85em', color: 'var(--text-muted)' }}>
177
+ 💡 <strong>Tip:</strong> You can create the first admin by manually changing <code>"role": "user"</code> to <code>"role": "admin"</code> in <code>easy-db/user.json</code> for your user record.
178
+ </div>
179
+ </>
180
+ ) : (
181
+ <>
182
+ <h2>Access Denied</h2>
183
+ <p>You do not have administrative permissions. (Logged in as <strong>{user?.name || user?.email}</strong>)</p>
184
+ <div style={{ display: 'flex', gap: 15, justifyContent: 'center', marginTop: 20 }}>
185
+ <button onClick={() => loginUser(null)}>Log Out</button>
186
+ <Link href="/">Back to website</Link>
187
+ </div>
188
+ </>
189
+ )}
190
+ </div>
191
+ );
192
+ }
193
+ // --- ADMIN_WITH_AUTH END ---
194
+
195
+ // --- ADMIN_NO_AUTH START ---
196
+ const [password, setPassword] = useState("");
197
+ const [loggedIn, setLoggedIn] = useState(false);
198
+
199
+ // --- ADMIN_ERRORS_NO_AUTH START ---
200
+ const fetchErrors = async (pwd: string, m: string) => {
201
+ setLoadingErrors(true);
202
+ setErrorLogsError("");
203
+ try {
204
+ const [data, err] = await api.getEasyAnalyticsErrors({ password: pwd, month: m });
205
+ if (data) {
206
+ setErrorsList(data);
207
+ } else if (err) {
208
+ setErrorLogsError(err.message || String(err));
209
+ }
210
+ } catch (e) {
211
+ setErrorLogsError(String(e));
212
+ }
213
+ setLoadingErrors(false);
214
+ };
215
+ // --- ADMIN_ERRORS_NO_AUTH END ---
18
216
 
19
217
  useEffect(() => {
20
218
  const savedPwd = localStorage.getItem("admin_pwd");
@@ -28,17 +226,31 @@ export default function Admin() {
28
226
  setLoading(true);
29
227
  setError("");
30
228
  try {
229
+ // --- ANALYTICS START ---
31
230
  const [data, err] = await api.getEasyAnalyticsData({ password: pwd, month: m });
32
-
33
231
  if (data) {
34
232
  setRecords(data as any);
35
233
  setLoggedIn(true);
36
234
  localStorage.setItem("admin_pwd", pwd);
37
235
  } else if (err) {
38
- setError(err);
236
+ setError(err.message || String(err));
237
+ setLoggedIn(false);
238
+ localStorage.removeItem("admin_pwd");
239
+ }
240
+ // --- ANALYTICS END ---
241
+ // --- NO_ANALYTICS START ---
242
+ // --- ADMIN_ERRORS START ---
243
+ const [data, err] = await api.getEasyAnalyticsErrors({ password: pwd, month: m });
244
+ if (data || !err) {
245
+ setLoggedIn(true);
246
+ localStorage.setItem("admin_pwd", pwd);
247
+ } else {
248
+ setError(err.message || String(err));
39
249
  setLoggedIn(false);
40
250
  localStorage.removeItem("admin_pwd");
41
251
  }
252
+ // --- ADMIN_ERRORS END ---
253
+ // --- NO_ANALYTICS END ---
42
254
  } catch (e) {
43
255
  setError(String(e));
44
256
  handleError(e as Error);
@@ -46,6 +258,19 @@ export default function Admin() {
46
258
  setLoading(false);
47
259
  };
48
260
 
261
+ useEffect(() => {
262
+ if (loggedIn) {
263
+ if (activeTab === "analytics") {
264
+ fetchData(password, month);
265
+ }
266
+ // --- ADMIN_ERRORS START ---
267
+ if (activeTab === "errors") {
268
+ fetchErrors(password, month);
269
+ }
270
+ // --- ADMIN_ERRORS END ---
271
+ }
272
+ }, [loggedIn, password, month, activeTab]);
273
+
49
274
  const handleLogin = (e: React.FormEvent) => {
50
275
  e.preventDefault();
51
276
  fetchData(password, month);
@@ -56,112 +281,162 @@ export default function Admin() {
56
281
  newMonth.setMonth(newMonth.getMonth() + move);
57
282
  const nextM = newMonth.toISOString().slice(0, 7);
58
283
  setMonth(nextM);
59
- if (loggedIn) fetchData(password, nextM);
60
- };
61
-
62
- const byURL = useGroup(records, (r: any) => {
63
- try {
64
- return new URL(r.url).pathname;
65
- } catch (e) {
66
- return r.url || "Unknown";
284
+ if (loggedIn) {
285
+ if (activeTab === "analytics") {
286
+ fetchData(password, nextM);
287
+ }
288
+ // --- ADMIN_ERRORS START ---
289
+ if (activeTab === "errors") {
290
+ fetchErrors(password, nextM);
291
+ }
292
+ // --- ADMIN_ERRORS END ---
67
293
  }
68
- });
69
-
70
- const byReferrer = useGroup(records, (r: any) => r.referrer || "Direct", (rs: any) => getGroupedCount(rs, (row: any) => row.localId));
71
-
72
- const byWidth = useGroup(records, (r: any) => {
73
- const w = r.window?.innerWidth || 0;
74
- if (w >= 1920) return ">= 1920px Full HD";
75
- if (w >= 1280) return "1280px - 1920px HD";
76
- if (w >= 768) return "768px - 1280px Tablet";
77
- if (w > 0) return "< 768px Mobile";
78
- return "Unknown";
79
- });
80
-
81
- // Simplified UA parser - for production consider using a library like 'ua-parser-js'
82
- const byBrowserLike = useGroup(records, (r: any) => {
83
- const ua = (r.userAgent || "").toLowerCase();
84
- if (ua.includes("firefox")) return "Firefox";
85
- if (ua.includes("edg")) return "Edge";
86
- if (ua.includes("chrome")) return "Chrome";
87
- if (ua.includes("safari")) return "Safari";
88
- return "Other/Unknown";
89
- }, (rs: any) => getGroupedCount(rs, (row: any) => row.localId));
294
+ };
90
295
 
91
296
  if (!loggedIn) {
92
297
  return (
93
- <div className="admin-login">
94
- <h2>Analytics Login</h2>
95
- <div className="back-link">
96
- <Link href="/">Back to website</Link>
97
- </div>
98
- <form onSubmit={handleLogin}>
298
+ <div className="admin-login" style={{ padding: 40, textAlign: 'center' }}>
299
+ <h2>Admin Login Required</h2>
300
+ <form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: 10, maxWidth: 300, margin: '20px auto' }}>
99
301
  <input
100
302
  type="password"
101
303
  placeholder="Enter admin password"
102
304
  value={password}
103
305
  onChange={e => setPassword(e.target.value)}
306
+ style={{ padding: 10, background: 'var(--surface)', border: '1px solid var(--border)', color: 'var(--text)', borderRadius: 4 }}
307
+ required
104
308
  />
105
- <button type="submit">Login</button>
106
- {error && <p style={{ color: "#ef4444", marginTop: "10px" }}>{error}</p>}
309
+ <button type="submit" disabled={loading} style={{ padding: 10, background: 'var(--primary)', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer' }}>
310
+ {loading ? "Logging in..." : "Login"}
311
+ </button>
107
312
  </form>
313
+ {error && <p style={{ color: "#ef4444" }}>{error}</p>}
314
+ <div style={{ marginTop: 20 }}>
315
+ <Link href="/">Back to website</Link>
316
+ </div>
108
317
  </div>
109
318
  );
110
319
  }
111
-
112
- // Daily visits counter
113
- const daysInMonth = new Date(parseInt(month.slice(0, 4)), parseInt(month.slice(5, 7)), 0).getDate();
114
- const dailyStats = Array.from({ length: daysInMonth }, (_, i) => {
115
- const d = i + 1;
116
- const dateStr = `${month}-${d.toString().padStart(2, "0")}`;
117
- const count = records.filter((r: any) => r.serverDate && r.serverDate.startsWith(dateStr)).length;
118
- return { date: d, count };
119
- });
120
- const maxVisits = Math.max(...dailyStats.map(s => s.count), 1);
320
+ // --- ADMIN_NO_AUTH END ---
121
321
 
122
322
  return (
123
323
  <div className="admin-container">
124
324
  <div className="header">
125
- <h2>Analytics ({records.length} events)</h2>
325
+ <h2>Admin Dashboard</h2>
126
326
  <Link href="/">Back to website</Link>
127
327
  </div>
128
328
 
329
+ <div className="admin-tabs" style={{ display: 'flex', gap: 10, margin: '20px 0', borderBottom: '1px solid var(--border)', paddingBottom: 10 }}>
330
+ {/* --- ANALYTICS START --- */}
331
+ <Link
332
+ href="/admin/analytics"
333
+ style={{
334
+ padding: '8px 16px',
335
+ background: activeTab === "analytics" ? 'var(--primary)' : 'transparent',
336
+ color: activeTab === "analytics" ? '#fff' : 'var(--text-muted)',
337
+ border: 'none',
338
+ borderRadius: 4,
339
+ cursor: 'pointer',
340
+ fontWeight: 'bold',
341
+ textDecoration: 'none'
342
+ }}
343
+ >
344
+ Analytics
345
+ </Link>
346
+ {/* --- ANALYTICS END --- */}
347
+ {/* --- ADMIN_ERRORS START --- */}
348
+ <Link
349
+ href="/admin/errors"
350
+ style={{
351
+ padding: '8px 16px',
352
+ background: activeTab === "errors" ? 'var(--primary)' : 'transparent',
353
+ color: activeTab === "errors" ? '#fff' : 'var(--text-muted)',
354
+ border: 'none',
355
+ borderRadius: 4,
356
+ cursor: 'pointer',
357
+ fontWeight: 'bold',
358
+ textDecoration: 'none'
359
+ }}
360
+ >
361
+ Error Logs
362
+ </Link>
363
+ {/* --- ADMIN_ERRORS END --- */}
364
+ {/* --- ADMIN_USERS START --- */}
365
+ <Link
366
+ href="/admin/users"
367
+ style={{
368
+ padding: '8px 16px',
369
+ background: activeTab === "users" ? 'var(--primary)' : 'transparent',
370
+ color: activeTab === "users" ? '#fff' : 'var(--text-muted)',
371
+ border: 'none',
372
+ borderRadius: 4,
373
+ cursor: 'pointer',
374
+ fontWeight: 'bold',
375
+ textDecoration: 'none'
376
+ }}
377
+ >
378
+ User Management
379
+ </Link>
380
+ {/* --- ADMIN_USERS END --- */}
381
+ </div>
382
+
129
383
  <div className="controls">
130
- <button onClick={() => handleMonthChange(-1)}>{"< Month"}</button>
384
+ {/* --- ANALYTICS START --- */}
385
+ <button onClick={() => handleMonthChange(-1)}>← Previous Month</button>
386
+ <strong>{month}</strong>
387
+ <button onClick={() => handleMonthChange(1)}>Next Month →</button>
388
+ {/* --- ANALYTICS END --- */}
389
+ {/* --- NO_ANALYTICS START --- */}
390
+ {/* --- ADMIN_ERRORS START --- */}
391
+ <button onClick={() => handleMonthChange(-1)}>← Previous Month</button>
131
392
  <strong>{month}</strong>
132
- <button onClick={() => handleMonthChange(1)}>{"Month >"}</button>
133
- <button onClick={() => fetchData(password, month)} disabled={loading}>
134
- {loading ? "Loading..." : "Refresh"}
135
- </button>
393
+ <button onClick={() => handleMonthChange(1)}>Next Month →</button>
394
+ {/* --- ADMIN_ERRORS END --- */}
395
+ {/* --- NO_ANALYTICS END --- */}
396
+
397
+ {/* --- ADMIN_WITH_AUTH START --- */}
398
+ <button className="btn-logout" onClick={() => loginUser(null)}>Log Out</button>
399
+ {/* --- ADMIN_WITH_AUTH END --- */}
400
+
401
+ {/* --- ADMIN_NO_AUTH START --- */}
136
402
  <button className="btn-logout" onClick={() => {
137
- setLoggedIn(false);
138
- setPassword("");
139
403
  localStorage.removeItem("admin_pwd");
140
- }}>Logout</button>
404
+ window.location.reload();
405
+ }}>Log Out</button>
406
+ {/* --- ADMIN_NO_AUTH END --- */}
141
407
  </div>
142
408
 
143
- <div className="chart-container">
144
- <h3>Pageviews per day</h3>
145
- <div className="chart">
146
- {dailyStats.map(({ date, count }) => (
147
- <div key={date} className="bar-wrapper">
148
- <span className="count">{count > 0 ? count : ""}</span>
149
- <div className={`bar ${count > 0 ? 'active' : ''}`} style={{
150
- height: `${(count / maxVisits) * 100}%`,
151
- minHeight: count > 0 ? "4px" : "0",
152
- }}></div>
153
- <span className="date">{date}.</span>
154
- </div>
155
- ))}
156
- </div>
157
- </div>
409
+ {loading && activeTab === "analytics" && <p style={{ textAlign: "center" }}>Loading...</p>}
410
+ {error && activeTab === "analytics" && <p style={{ color: "#ef4444", textAlign: "center" }}>{error}</p>}
411
+
412
+ {/* --- ANALYTICS START --- */}
413
+ {activeTab === "analytics" && (
414
+ <AnalyticsTab records={records} month={month} />
415
+ )}
416
+ {/* --- ANALYTICS END --- */}
417
+
418
+ {/* --- ADMIN_ERRORS START --- */}
419
+ {activeTab === "errors" && (
420
+ <ErrorsTab errors={errorsList} loading={loadingErrors} errorMsg={errorLogsError} />
421
+ )}
422
+ {/* --- ADMIN_ERRORS END --- */}
423
+
424
+ {/* --- ADMIN_USERS START --- */}
425
+ {activeTab === "users" && (
426
+ <UsersTab users={usersList} loading={loadingUsers} errorMsg={usersError} onRoleChange={handleRoleChange} />
427
+ )}
428
+ {/* --- ADMIN_USERS END --- */}
158
429
 
159
- <div className="grid">
160
- <GroupTable title="Pageviews" data={byURL} mainColor="#6366f1" />
161
- <GroupTable title="Traffic Sources (Unique)" data={byReferrer} mainColor="#34d399" />
162
- <GroupTable title="Screen Resolution" data={byWidth} mainColor="#fbbf24" />
163
- <GroupTable title="Browser (Unique)" data={byBrowserLike} mainColor="#c084fc" />
430
+ {/* --- NO_ANALYTICS START --- */}
431
+ {/* --- NO_ADMIN_ERRORS START --- */}
432
+ {/* --- NO_ADMIN_USERS START --- */}
433
+ <div style={{ textAlign: 'center', padding: 40, border: '1px dashed var(--border)', borderRadius: 8 }}>
434
+ <h3>Generic Admin Panel</h3>
435
+ <p style={{ color: 'var(--text-muted)' }}>Welcome to the admin panel.</p>
164
436
  </div>
437
+ {/* --- NO_ADMIN_USERS END --- */}
438
+ {/* --- NO_ADMIN_ERRORS END --- */}
439
+ {/* --- NO_ANALYTICS END --- */}
165
440
  </div>
166
441
  );
167
442
  }
@@ -0,0 +1,71 @@
1
+ import React from "react";
2
+ import { useGroup, GroupTable, getGroupedCount } from "easy-analytics/react";
3
+
4
+ type AnalyticsTabProps = {
5
+ records: any[];
6
+ month: string;
7
+ };
8
+
9
+ export default function AnalyticsTab({ records, month }: AnalyticsTabProps) {
10
+ const byURL = useGroup(records, (r: any) => {
11
+ try {
12
+ return new URL(r.url).pathname;
13
+ } catch (e) {
14
+ return r.url || "Unknown";
15
+ }
16
+ });
17
+
18
+ const byReferrer = useGroup(records, (r: any) => r.referrer || "Direct", (rs: any) => getGroupedCount(rs, (row: any) => row.localId));
19
+
20
+ const byWidth = useGroup(records, (r: any) => {
21
+ const w = r.window?.innerWidth || 0;
22
+ if (w >= 1920) return ">= 1920px Full HD";
23
+ if (w >= 1280) return "1280px - 1920px HD";
24
+ if (w >= 768) return "768px - 1280px Tablet";
25
+ if (w > 0) return "< 768px Mobile";
26
+ return "Unknown";
27
+ });
28
+
29
+ const byBrowserLike = useGroup(records, (r: any) => {
30
+ const ua = (r.userAgent || "").toLowerCase();
31
+ if (ua.includes("firefox")) return "Firefox";
32
+ if (ua.includes("edg")) return "Edge";
33
+ if (ua.includes("chrome")) return "Chrome";
34
+ if (ua.includes("safari")) return "Safari";
35
+ return "Other/Unknown";
36
+ }, (rs: any) => getGroupedCount(rs, (row: any) => row.localId));
37
+
38
+ return (
39
+ <div>
40
+ <div className="chart-container">
41
+ <h3>Visits per day</h3>
42
+ <div className="chart">
43
+ {Array.from({ length: new Date(month.slice(0, 4) as any, month.slice(5, 7) as any, 0).getDate() }, (_, i) => {
44
+ const dayStr = String(i + 1).padStart(2, "0");
45
+ const fullDate = `${month}-${dayStr}`;
46
+ const count = records.filter((r: any) => r.created && r.created.slice(0, 10) === fullDate).length;
47
+ const maxCount = Math.max(...Array.from({ length: 31 }, (_, dayIndex) => {
48
+ const dStr = String(dayIndex + 1).padStart(2, "0");
49
+ return records.filter((r: any) => r.created && r.created.slice(0, 10) === `${month}-${dStr}`).length;
50
+ }));
51
+ const heightPercent = maxCount > 0 ? (count / maxCount) * 80 : 0;
52
+ return (
53
+ <div key={i} className="bar-wrapper">
54
+ <span className="count">{count || ""}</span>
55
+ <div className="bar active" style={{ height: `${heightPercent}%` }}></div>
56
+ <span className="date">{i + 1}</span>
57
+ </div>
58
+ );
59
+ })}
60
+ </div>
61
+ </div>
62
+
63
+ <div className="grid">
64
+ <GroupTable title="Most visited paths" data={byURL} columnName="Path" />
65
+ <GroupTable title="Referrers" data={byReferrer} columnName="Source" />
66
+ <GroupTable title="Browsers" data={byBrowserLike} columnName="Browser" />
67
+ <GroupTable title="Display resolutions" data={byWidth} columnName="Resolution" />
68
+ </div>
69
+ </div>
70
+ );
71
+ }
@@ -0,0 +1,46 @@
1
+ import React from "react";
2
+
3
+ type ErrorsTabProps = {
4
+ errors: any[];
5
+ loading: boolean;
6
+ errorMsg: string;
7
+ };
8
+
9
+ export default function ErrorsTab({ errors, loading, errorMsg }: ErrorsTabProps) {
10
+ return (
11
+ <div>
12
+ <h3 style={{ marginBottom: 15 }}>Logged Errors</h3>
13
+ {loading && <p>Loading error logs...</p>}
14
+ {errorMsg && <p style={{ color: "#ef4444" }}>{errorMsg}</p>}
15
+ {!loading && !errorMsg && errors.length === 0 && (
16
+ <p style={{ color: "var(--text-muted)" }}>No errors logged for this month.</p>
17
+ )}
18
+ {!loading && !errorMsg && errors.length > 0 && (
19
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 15 }}>
20
+ {errors.map((errRecord: any, i: number) => (
21
+ <div key={i} style={{ padding: 15, background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 6 }}>
22
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10, fontSize: '0.9em' }}>
23
+ <span style={{ color: 'var(--primary)', fontWeight: 'bold' }}>{errRecord.name || "Error"}</span>
24
+ <span style={{ color: 'var(--text-muted)' }}>{errRecord.created ? new Date(errRecord.created).toLocaleString() : ""}</span>
25
+ </div>
26
+ <p style={{ margin: '5px 0', fontFamily: 'monospace', whiteSpace: 'pre-wrap', wordBreak: 'break-all', color: '#f87171' }}>
27
+ {errRecord.message}
28
+ </p>
29
+ {errRecord.stack && (
30
+ <details style={{ marginTop: 10 }}>
31
+ <summary style={{ cursor: 'pointer', color: 'var(--text-muted)', fontSize: '0.85em' }}>Show Stack Trace</summary>
32
+ <pre style={{ marginTop: 5, padding: 10, background: '#09090b', overflowX: 'auto', fontSize: '0.8em', color: 'var(--text-muted)' }}>
33
+ {errRecord.stack}
34
+ </pre>
35
+ </details>
36
+ )}
37
+ <div style={{ marginTop: 10, fontSize: '0.8em', color: 'var(--text-muted)' }}>
38
+ URL: {errRecord.url} | User Agent: {errRecord.userAgent}
39
+ </div>
40
+ </div>
41
+ ))}
42
+ </div>
43
+ )}
44
+ </div>
45
+ );
46
+ }