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.
- package/README.md +33 -80
- package/bin/index.js +251 -49
- package/package.json +20 -11
- package/template/.cursorrules +42 -10
- package/template/README.md +129 -25
- package/template/env +0 -1
- package/template/gitignore +2 -1
- package/template/index.html +15 -0
- package/template/package.json +14 -8
- package/template/{src → public}/site.webmanifest +6 -12
- package/template/scripts/deploy.ts +3 -2
- package/template/server/index.tsx +264 -56
- package/template/server/mail.tsx +120 -0
- package/template/src/App.tsx +33 -7
- package/template/src/Router.tsx +13 -4
- package/template/src/components/Img.tsx +127 -0
- package/template/src/index.tsx +28 -1
- package/template/src/pages/Index.tsx +36 -6
- package/template/src/pages/NotFound.tsx +1 -1
- package/template/src/pages/ResetPassword.tsx +124 -0
- package/template/src/pages/admin/Admin.tsx +332 -85
- package/template/src/pages/admin/AnalyticsTab.tsx +71 -0
- package/template/src/pages/admin/ErrorsTab.tsx +46 -0
- package/template/src/pages/admin/UsersTab.tsx +54 -0
- package/template/src/services/api.ts +5 -7
- package/template/src/services/common.ts +34 -1
- package/template/src/styles.css +667 -0
- package/template/tsconfig.json +7 -2
- package/template/types.ts +44 -3
- package/template/vite.config.ts +29 -0
- package/template/.parcelrc +0 -6
- package/template/src/images/icon.png +0 -0
- package/template/src/index.html +0 -16
- 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
|
-
|
|
9
|
-
|
|
7
|
+
// --- ANALYTICS START ---
|
|
8
|
+
import AnalyticsTab from "./AnalyticsTab";
|
|
9
|
+
// --- ANALYTICS END ---
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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>
|
|
95
|
-
<
|
|
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">
|
|
106
|
-
|
|
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>
|
|
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
|
-
|
|
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)}>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
376
|
+
window.location.reload();
|
|
377
|
+
}}>Log Out</button>
|
|
378
|
+
{/* --- ADMIN_NO_AUTH END --- */}
|
|
141
379
|
</div>
|
|
142
380
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
<
|
|
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
|
+
}
|