easy-starter 1.2.1 → 2.1.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.
@@ -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 as any)?.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 as any)?.message || String(err)));
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 as any)?.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 as any)?.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">
168
+ {!isLoggedIn ? (
169
+ <>
170
+ <h2>Admin Login Required</h2>
171
+ <p>Please log in using an account with administrative permissions.</p>
172
+ <div className="btn-center-group">
173
+ <button onClick={showUserDialog}>Log In</button>
174
+ <Link href="/">Back to website</Link>
175
+ </div>
176
+ <div className="tip-box">
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 as any)?.email}</strong>)</p>
184
+ <div className="btn-center-group">
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 as any).getEasyAnalyticsErrors({ password: pwd, month: m });
205
+ if (data) {
206
+ setErrorsList(data);
207
+ } else if (err) {
208
+ setErrorLogsError((err as any)?.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 {
31
- const [data, err] = await api.getEasyAnalyticsData({ password: pwd, month: m });
32
-
229
+ // --- ANALYTICS START ---
230
+ const [data, err] = await (api as any).getEasyAnalyticsData({ password: pwd, month: m });
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 as any)?.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 [errData, fetchErr] = await (api as any).getEasyAnalyticsErrors({ password: pwd, month: m });
244
+ if (errData || !fetchErr) {
245
+ setLoggedIn(true);
246
+ localStorage.setItem("admin_pwd", pwd);
247
+ } else {
248
+ setError((fetchErr as any)?.message || String(fetchErr));
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,134 @@ 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
298
  <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}>
299
+ <h2>Admin Login Required</h2>
300
+ <form onSubmit={handleLogin} className="admin-login-form">
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
+ required
104
307
  />
105
- <button type="submit">Login</button>
106
- {error && <p style={{ color: "#ef4444", marginTop: "10px" }}>{error}</p>}
308
+ <button type="submit" disabled={loading}>
309
+ {loading ? "Logging in..." : "Login"}
310
+ </button>
107
311
  </form>
312
+ {error && <p className="text-error">{error}</p>}
313
+ <div className="admin-link-wrapper">
314
+ <Link href="/">Back to website</Link>
315
+ </div>
108
316
  </div>
109
317
  );
110
318
  }
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);
319
+ /* --- ADMIN_NO_AUTH END --- */
121
320
 
122
321
  return (
123
322
  <div className="admin-container">
124
323
  <div className="header">
125
- <h2>Analytics ({records.length} events)</h2>
324
+ <h2>Admin Dashboard</h2>
126
325
  <Link href="/">Back to website</Link>
127
326
  </div>
128
327
 
328
+ <div className="admin-tabs">
329
+ {/* --- ANALYTICS START --- */}
330
+ <Link
331
+ href="/admin/analytics"
332
+ className={`tab-btn ${activeTab === "analytics" ? "active" : ""}`}
333
+ >
334
+ Analytics
335
+ </Link>
336
+ {/* --- ANALYTICS END --- */}
337
+ {/* --- ADMIN_ERRORS START --- */}
338
+ <Link
339
+ href="/admin/errors"
340
+ className={`tab-btn ${activeTab === "errors" ? "active" : ""}`}
341
+ >
342
+ Error Logs
343
+ </Link>
344
+ {/* --- ADMIN_ERRORS END --- */}
345
+ {/* --- ADMIN_USERS START --- */}
346
+ <Link
347
+ href="/admin/users"
348
+ className={`tab-btn ${activeTab === "users" ? "active" : ""}`}
349
+ >
350
+ User Management
351
+ </Link>
352
+ {/* --- ADMIN_USERS END --- */}
353
+ </div>
354
+
129
355
  <div className="controls">
130
- <button onClick={() => handleMonthChange(-1)}>{"< Month"}</button>
356
+ {/* --- ANALYTICS START --- */}
357
+ <button onClick={() => handleMonthChange(-1)}>← Previous Month</button>
358
+ <strong>{month}</strong>
359
+ <button onClick={() => handleMonthChange(1)}>Next Month →</button>
360
+ {/* --- ANALYTICS END --- */}
361
+ {/* --- NO_ANALYTICS START --- */}
362
+ {/* --- ADMIN_ERRORS START --- */}
363
+ <button onClick={() => handleMonthChange(-1)}>← Previous Month</button>
131
364
  <strong>{month}</strong>
132
- <button onClick={() => handleMonthChange(1)}>{"Month >"}</button>
133
- <button onClick={() => fetchData(password, month)} disabled={loading}>
134
- {loading ? "Loading..." : "Refresh"}
135
- </button>
365
+ <button onClick={() => handleMonthChange(1)}>Next Month →</button>
366
+ {/* --- ADMIN_ERRORS END --- */}
367
+ {/* --- NO_ANALYTICS END --- */}
368
+
369
+ {/* --- ADMIN_WITH_AUTH START --- */}
370
+ <button className="btn-logout" onClick={() => loginUser(null)}>Log Out</button>
371
+ {/* --- ADMIN_WITH_AUTH END --- */}
372
+
373
+ {/* --- ADMIN_NO_AUTH START --- */}
136
374
  <button className="btn-logout" onClick={() => {
137
- setLoggedIn(false);
138
- setPassword("");
139
375
  localStorage.removeItem("admin_pwd");
140
- }}>Logout</button>
376
+ window.location.reload();
377
+ }}>Log Out</button>
378
+ {/* --- ADMIN_NO_AUTH END --- */}
141
379
  </div>
142
380
 
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>
381
+ {loading && activeTab === "analytics" && <p>Loading...</p>}
382
+ {error && activeTab === "analytics" && <p className="text-error">{error}</p>}
383
+
384
+ {/* --- ANALYTICS START --- */}
385
+ {activeTab === "analytics" && (
386
+ <AnalyticsTab records={records} month={month} />
387
+ )}
388
+ {/* --- ANALYTICS END --- */}
389
+
390
+ {/* --- ADMIN_ERRORS START --- */}
391
+ {activeTab === "errors" && (
392
+ <ErrorsTab errors={errorsList} loading={loadingErrors} errorMsg={errorLogsError} />
393
+ )}
394
+ {/* --- ADMIN_ERRORS END --- */}
395
+
396
+ {/* --- ADMIN_USERS START --- */}
397
+ {activeTab === "users" && (
398
+ <UsersTab users={usersList} loading={loadingUsers} errorMsg={usersError} onRoleChange={handleRoleChange} />
399
+ )}
400
+ {/* --- ADMIN_USERS END --- */}
158
401
 
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" />
402
+ {/* --- NO_ANALYTICS START --- */}
403
+ {/* --- NO_ADMIN_ERRORS START --- */}
404
+ {/* --- NO_ADMIN_USERS START --- */}
405
+ <div className="admin-card">
406
+ <h3>Generic Admin Panel</h3>
407
+ <p>Welcome to the admin panel.</p>
164
408
  </div>
409
+ {/* --- NO_ADMIN_USERS END --- */}
410
+ {/* --- NO_ADMIN_ERRORS END --- */}
411
+ {/* --- NO_ANALYTICS END --- */}
165
412
  </div>
166
413
  );
167
414
  }
@@ -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} />
65
+ <GroupTable title="Referrers" data={byReferrer} />
66
+ <GroupTable title="Browsers" data={byBrowserLike} />
67
+ <GroupTable title="Display resolutions" data={byWidth} />
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>Logged Errors</h3>
13
+ {loading && <p>Loading error logs...</p>}
14
+ {errorMsg && <p className="text-error">{errorMsg}</p>}
15
+ {!loading && !errorMsg && errors.length === 0 && (
16
+ <p>No errors logged for this month.</p>
17
+ )}
18
+ {!loading && !errorMsg && errors.length > 0 && (
19
+ <div className="error-list">
20
+ {errors.map((errRecord: any, i: number) => (
21
+ <div key={i} className="error-item">
22
+ <div className="error-item-header">
23
+ <span className="error-name">{errRecord.name || "Error"}</span>
24
+ <span>{errRecord.created ? new Date(errRecord.created).toLocaleString() : ""}</span>
25
+ </div>
26
+ <p className="error-message">
27
+ {errRecord.message}
28
+ </p>
29
+ {errRecord.stack && (
30
+ <details className="error-details">
31
+ <summary>Show Stack Trace</summary>
32
+ <pre>
33
+ {errRecord.stack}
34
+ </pre>
35
+ </details>
36
+ )}
37
+ <div>
38
+ URL: {errRecord.url} | User Agent: {errRecord.userAgent}
39
+ </div>
40
+ </div>
41
+ ))}
42
+ </div>
43
+ )}
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1,54 @@
1
+ import React from "react";
2
+
3
+ type UsersTabProps = {
4
+ users: any[];
5
+ loading: boolean;
6
+ errorMsg: string;
7
+ onRoleChange: (userId: string, role: "admin" | "user") => void;
8
+ };
9
+
10
+ export default function UsersTab({ users, loading, errorMsg, onRoleChange }: UsersTabProps) {
11
+ return (
12
+ <div>
13
+ <h3>User Management</h3>
14
+ {loading && <p>Loading users...</p>}
15
+ {errorMsg && <p className="text-error">{errorMsg}</p>}
16
+ {!loading && !errorMsg && (
17
+ <div className="table-wrapper">
18
+ <table className="admin-table">
19
+ <thead>
20
+ <tr>
21
+ <th>Email</th>
22
+ <th>Name</th>
23
+ <th>Role</th>
24
+ <th>Actions</th>
25
+ </tr>
26
+ </thead>
27
+ <tbody>
28
+ {users.map((usr: any) => (
29
+ <tr key={usr._id}>
30
+ <td>{usr.email}</td>
31
+ <td>{usr.name || "N/A"}</td>
32
+ <td>
33
+ <span className={`role-badge ${usr.role === 'admin' ? 'admin' : ''}`}>
34
+ {usr.role || "user"}
35
+ </span>
36
+ </td>
37
+ <td>
38
+ <select
39
+ value={usr.role || "user"}
40
+ onChange={(e) => onRoleChange(usr._id, e.target.value as any)}
41
+ >
42
+ <option value="user">User</option>
43
+ <option value="admin">Admin</option>
44
+ </select>
45
+ </td>
46
+ </tr>
47
+ ))}
48
+ </tbody>
49
+ </table>
50
+ </div>
51
+ )}
52
+ </div>
53
+ );
54
+ }