easy-starter 2.0.0 → 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 +5 -4
- package/bin/index.js +65 -38
- package/package.json +1 -1
- package/template/.cursorrules +32 -7
- package/template/README.md +48 -1
- package/template/gitignore +1 -0
- package/template/index.html +3 -3
- package/template/package.json +11 -4
- package/template/public/site.webmanifest +8 -8
- package/template/scripts/deploy.ts +3 -2
- package/template/server/index.tsx +42 -30
- package/template/src/Router.tsx +2 -2
- package/template/src/components/Img.tsx +127 -0
- package/template/src/pages/Index.tsx +4 -13
- package/template/src/pages/NotFound.tsx +1 -1
- package/template/src/pages/ResetPassword.tsx +6 -8
- package/template/src/pages/admin/Admin.tsx +31 -59
- package/template/src/pages/admin/AnalyticsTab.tsx +4 -4
- package/template/src/pages/admin/ErrorsTab.tsx +13 -13
- package/template/src/pages/admin/UsersTab.tsx +15 -22
- package/template/src/services/common.ts +11 -1
- package/template/src/styles.css +207 -0
- package/template/tsconfig.json +5 -1
- package/template/types.ts +4 -4
- package/template/vite.config.ts +7 -2
- package/template/public/images/icon-16.png +0 -0
- package/template/public/images/icon-180.png +0 -0
- package/template/public/images/icon-192.png +0 -0
- package/template/public/images/icon-32.png +0 -0
- package/template/public/images/icon-512.png +0 -0
|
@@ -78,7 +78,7 @@ export default function Admin({ path }: AdminProps) {
|
|
|
78
78
|
if (data) {
|
|
79
79
|
setUsersList(data);
|
|
80
80
|
} else if (err) {
|
|
81
|
-
setUsersError(err
|
|
81
|
+
setUsersError((err as any)?.message || String(err));
|
|
82
82
|
}
|
|
83
83
|
} catch (e) {
|
|
84
84
|
setUsersError(String(e));
|
|
@@ -92,7 +92,7 @@ export default function Admin({ path }: AdminProps) {
|
|
|
92
92
|
if (success) {
|
|
93
93
|
setUsersList(prev => prev.map(u => u._id === userId ? { ...u, role: newRole } : u));
|
|
94
94
|
} else if (err) {
|
|
95
|
-
alert("Failed to update role: " + err
|
|
95
|
+
alert("Failed to update role: " + ((err as any)?.message || String(err)));
|
|
96
96
|
}
|
|
97
97
|
} catch (e) {
|
|
98
98
|
alert(String(e));
|
|
@@ -109,7 +109,7 @@ export default function Admin({ path }: AdminProps) {
|
|
|
109
109
|
if (data) {
|
|
110
110
|
setErrorsList(data);
|
|
111
111
|
} else if (err) {
|
|
112
|
-
setErrorLogsError(err
|
|
112
|
+
setErrorLogsError((err as any)?.message || String(err));
|
|
113
113
|
}
|
|
114
114
|
} catch (e) {
|
|
115
115
|
setErrorLogsError(String(e));
|
|
@@ -145,7 +145,7 @@ export default function Admin({ path }: AdminProps) {
|
|
|
145
145
|
if (data) {
|
|
146
146
|
setRecords(data as any);
|
|
147
147
|
} else if (err) {
|
|
148
|
-
setError(err
|
|
148
|
+
setError((err as any)?.message || String(err));
|
|
149
149
|
}
|
|
150
150
|
// --- ANALYTICS END ---
|
|
151
151
|
} catch (e) {
|
|
@@ -164,24 +164,24 @@ export default function Admin({ path }: AdminProps) {
|
|
|
164
164
|
|
|
165
165
|
if (!isLoggedIn || user?.role !== "admin") {
|
|
166
166
|
return (
|
|
167
|
-
<div className="admin-login"
|
|
167
|
+
<div className="admin-login">
|
|
168
168
|
{!isLoggedIn ? (
|
|
169
169
|
<>
|
|
170
170
|
<h2>Admin Login Required</h2>
|
|
171
171
|
<p>Please log in using an account with administrative permissions.</p>
|
|
172
|
-
<div
|
|
172
|
+
<div className="btn-center-group">
|
|
173
173
|
<button onClick={showUserDialog}>Log In</button>
|
|
174
174
|
<Link href="/">Back to website</Link>
|
|
175
175
|
</div>
|
|
176
|
-
<div
|
|
176
|
+
<div className="tip-box">
|
|
177
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
178
|
</div>
|
|
179
179
|
</>
|
|
180
180
|
) : (
|
|
181
181
|
<>
|
|
182
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
|
|
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
185
|
<button onClick={() => loginUser(null)}>Log Out</button>
|
|
186
186
|
<Link href="/">Back to website</Link>
|
|
187
187
|
</div>
|
|
@@ -192,7 +192,7 @@ export default function Admin({ path }: AdminProps) {
|
|
|
192
192
|
}
|
|
193
193
|
// --- ADMIN_WITH_AUTH END ---
|
|
194
194
|
|
|
195
|
-
|
|
195
|
+
/* --- ADMIN_NO_AUTH START --- */
|
|
196
196
|
const [password, setPassword] = useState("");
|
|
197
197
|
const [loggedIn, setLoggedIn] = useState(false);
|
|
198
198
|
|
|
@@ -201,11 +201,11 @@ export default function Admin({ path }: AdminProps) {
|
|
|
201
201
|
setLoadingErrors(true);
|
|
202
202
|
setErrorLogsError("");
|
|
203
203
|
try {
|
|
204
|
-
const [data, err] = await api.getEasyAnalyticsErrors({ password: pwd, month: m });
|
|
204
|
+
const [data, err] = await (api as any).getEasyAnalyticsErrors({ password: pwd, month: m });
|
|
205
205
|
if (data) {
|
|
206
206
|
setErrorsList(data);
|
|
207
207
|
} else if (err) {
|
|
208
|
-
setErrorLogsError(err
|
|
208
|
+
setErrorLogsError((err as any)?.message || String(err));
|
|
209
209
|
}
|
|
210
210
|
} catch (e) {
|
|
211
211
|
setErrorLogsError(String(e));
|
|
@@ -227,25 +227,25 @@ export default function Admin({ path }: AdminProps) {
|
|
|
227
227
|
setError("");
|
|
228
228
|
try {
|
|
229
229
|
// --- ANALYTICS START ---
|
|
230
|
-
const [data, err] = await api.getEasyAnalyticsData({ password: pwd, month: m });
|
|
230
|
+
const [data, err] = await (api as any).getEasyAnalyticsData({ password: pwd, month: m });
|
|
231
231
|
if (data) {
|
|
232
232
|
setRecords(data as any);
|
|
233
233
|
setLoggedIn(true);
|
|
234
234
|
localStorage.setItem("admin_pwd", pwd);
|
|
235
235
|
} else if (err) {
|
|
236
|
-
setError(err
|
|
236
|
+
setError((err as any)?.message || String(err));
|
|
237
237
|
setLoggedIn(false);
|
|
238
238
|
localStorage.removeItem("admin_pwd");
|
|
239
239
|
}
|
|
240
240
|
// --- ANALYTICS END ---
|
|
241
241
|
// --- NO_ANALYTICS START ---
|
|
242
242
|
// --- ADMIN_ERRORS START ---
|
|
243
|
-
const [
|
|
244
|
-
if (
|
|
243
|
+
const [errData, fetchErr] = await (api as any).getEasyAnalyticsErrors({ password: pwd, month: m });
|
|
244
|
+
if (errData || !fetchErr) {
|
|
245
245
|
setLoggedIn(true);
|
|
246
246
|
localStorage.setItem("admin_pwd", pwd);
|
|
247
247
|
} else {
|
|
248
|
-
setError(
|
|
248
|
+
setError((fetchErr as any)?.message || String(fetchErr));
|
|
249
249
|
setLoggedIn(false);
|
|
250
250
|
localStorage.removeItem("admin_pwd");
|
|
251
251
|
}
|
|
@@ -295,29 +295,28 @@ export default function Admin({ path }: AdminProps) {
|
|
|
295
295
|
|
|
296
296
|
if (!loggedIn) {
|
|
297
297
|
return (
|
|
298
|
-
<div className="admin-login"
|
|
298
|
+
<div className="admin-login">
|
|
299
299
|
<h2>Admin Login Required</h2>
|
|
300
|
-
<form onSubmit={handleLogin}
|
|
300
|
+
<form onSubmit={handleLogin} className="admin-login-form">
|
|
301
301
|
<input
|
|
302
302
|
type="password"
|
|
303
303
|
placeholder="Enter admin password"
|
|
304
304
|
value={password}
|
|
305
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
306
|
required
|
|
308
307
|
/>
|
|
309
|
-
<button type="submit" disabled={loading}
|
|
308
|
+
<button type="submit" disabled={loading}>
|
|
310
309
|
{loading ? "Logging in..." : "Login"}
|
|
311
310
|
</button>
|
|
312
311
|
</form>
|
|
313
|
-
{error && <p
|
|
314
|
-
<div
|
|
312
|
+
{error && <p className="text-error">{error}</p>}
|
|
313
|
+
<div className="admin-link-wrapper">
|
|
315
314
|
<Link href="/">Back to website</Link>
|
|
316
315
|
</div>
|
|
317
316
|
</div>
|
|
318
317
|
);
|
|
319
318
|
}
|
|
320
|
-
|
|
319
|
+
/* --- ADMIN_NO_AUTH END --- */
|
|
321
320
|
|
|
322
321
|
return (
|
|
323
322
|
<div className="admin-container">
|
|
@@ -326,20 +325,11 @@ export default function Admin({ path }: AdminProps) {
|
|
|
326
325
|
<Link href="/">Back to website</Link>
|
|
327
326
|
</div>
|
|
328
327
|
|
|
329
|
-
<div className="admin-tabs"
|
|
328
|
+
<div className="admin-tabs">
|
|
330
329
|
{/* --- ANALYTICS START --- */}
|
|
331
330
|
<Link
|
|
332
331
|
href="/admin/analytics"
|
|
333
|
-
|
|
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
|
-
}}
|
|
332
|
+
className={`tab-btn ${activeTab === "analytics" ? "active" : ""}`}
|
|
343
333
|
>
|
|
344
334
|
Analytics
|
|
345
335
|
</Link>
|
|
@@ -347,16 +337,7 @@ export default function Admin({ path }: AdminProps) {
|
|
|
347
337
|
{/* --- ADMIN_ERRORS START --- */}
|
|
348
338
|
<Link
|
|
349
339
|
href="/admin/errors"
|
|
350
|
-
|
|
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
|
-
}}
|
|
340
|
+
className={`tab-btn ${activeTab === "errors" ? "active" : ""}`}
|
|
360
341
|
>
|
|
361
342
|
Error Logs
|
|
362
343
|
</Link>
|
|
@@ -364,16 +345,7 @@ export default function Admin({ path }: AdminProps) {
|
|
|
364
345
|
{/* --- ADMIN_USERS START --- */}
|
|
365
346
|
<Link
|
|
366
347
|
href="/admin/users"
|
|
367
|
-
|
|
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
|
-
}}
|
|
348
|
+
className={`tab-btn ${activeTab === "users" ? "active" : ""}`}
|
|
377
349
|
>
|
|
378
350
|
User Management
|
|
379
351
|
</Link>
|
|
@@ -406,8 +378,8 @@ export default function Admin({ path }: AdminProps) {
|
|
|
406
378
|
{/* --- ADMIN_NO_AUTH END --- */}
|
|
407
379
|
</div>
|
|
408
380
|
|
|
409
|
-
{loading && activeTab === "analytics" && <p
|
|
410
|
-
{error && activeTab === "analytics" && <p
|
|
381
|
+
{loading && activeTab === "analytics" && <p>Loading...</p>}
|
|
382
|
+
{error && activeTab === "analytics" && <p className="text-error">{error}</p>}
|
|
411
383
|
|
|
412
384
|
{/* --- ANALYTICS START --- */}
|
|
413
385
|
{activeTab === "analytics" && (
|
|
@@ -430,9 +402,9 @@ export default function Admin({ path }: AdminProps) {
|
|
|
430
402
|
{/* --- NO_ANALYTICS START --- */}
|
|
431
403
|
{/* --- NO_ADMIN_ERRORS START --- */}
|
|
432
404
|
{/* --- NO_ADMIN_USERS START --- */}
|
|
433
|
-
<div
|
|
405
|
+
<div className="admin-card">
|
|
434
406
|
<h3>Generic Admin Panel</h3>
|
|
435
|
-
<p
|
|
407
|
+
<p>Welcome to the admin panel.</p>
|
|
436
408
|
</div>
|
|
437
409
|
{/* --- NO_ADMIN_USERS END --- */}
|
|
438
410
|
{/* --- NO_ADMIN_ERRORS END --- */}
|
|
@@ -61,10 +61,10 @@ export default function AnalyticsTab({ records, month }: AnalyticsTabProps) {
|
|
|
61
61
|
</div>
|
|
62
62
|
|
|
63
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}
|
|
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
68
|
</div>
|
|
69
69
|
</div>
|
|
70
70
|
);
|
|
@@ -9,32 +9,32 @@ type ErrorsTabProps = {
|
|
|
9
9
|
export default function ErrorsTab({ errors, loading, errorMsg }: ErrorsTabProps) {
|
|
10
10
|
return (
|
|
11
11
|
<div>
|
|
12
|
-
<h3
|
|
12
|
+
<h3>Logged Errors</h3>
|
|
13
13
|
{loading && <p>Loading error logs...</p>}
|
|
14
|
-
{errorMsg && <p
|
|
14
|
+
{errorMsg && <p className="text-error">{errorMsg}</p>}
|
|
15
15
|
{!loading && !errorMsg && errors.length === 0 && (
|
|
16
|
-
<p
|
|
16
|
+
<p>No errors logged for this month.</p>
|
|
17
17
|
)}
|
|
18
18
|
{!loading && !errorMsg && errors.length > 0 && (
|
|
19
|
-
<div
|
|
19
|
+
<div className="error-list">
|
|
20
20
|
{errors.map((errRecord: any, i: number) => (
|
|
21
|
-
<div key={i}
|
|
22
|
-
<div
|
|
23
|
-
<span
|
|
24
|
-
<span
|
|
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
25
|
</div>
|
|
26
|
-
<p
|
|
26
|
+
<p className="error-message">
|
|
27
27
|
{errRecord.message}
|
|
28
28
|
</p>
|
|
29
29
|
{errRecord.stack && (
|
|
30
|
-
<details
|
|
31
|
-
<summary
|
|
32
|
-
<pre
|
|
30
|
+
<details className="error-details">
|
|
31
|
+
<summary>Show Stack Trace</summary>
|
|
32
|
+
<pre>
|
|
33
33
|
{errRecord.stack}
|
|
34
34
|
</pre>
|
|
35
35
|
</details>
|
|
36
36
|
)}
|
|
37
|
-
<div
|
|
37
|
+
<div>
|
|
38
38
|
URL: {errRecord.url} | User Agent: {errRecord.userAgent}
|
|
39
39
|
</div>
|
|
40
40
|
</div>
|
|
@@ -10,41 +10,34 @@ type UsersTabProps = {
|
|
|
10
10
|
export default function UsersTab({ users, loading, errorMsg, onRoleChange }: UsersTabProps) {
|
|
11
11
|
return (
|
|
12
12
|
<div>
|
|
13
|
-
<h3
|
|
13
|
+
<h3>User Management</h3>
|
|
14
14
|
{loading && <p>Loading users...</p>}
|
|
15
|
-
{errorMsg && <p
|
|
15
|
+
{errorMsg && <p className="text-error">{errorMsg}</p>}
|
|
16
16
|
{!loading && !errorMsg && (
|
|
17
|
-
<div
|
|
18
|
-
<table
|
|
17
|
+
<div className="table-wrapper">
|
|
18
|
+
<table className="admin-table">
|
|
19
19
|
<thead>
|
|
20
|
-
<tr
|
|
21
|
-
<th
|
|
22
|
-
<th
|
|
23
|
-
<th
|
|
24
|
-
<th
|
|
20
|
+
<tr>
|
|
21
|
+
<th>Email</th>
|
|
22
|
+
<th>Name</th>
|
|
23
|
+
<th>Role</th>
|
|
24
|
+
<th>Actions</th>
|
|
25
25
|
</tr>
|
|
26
26
|
</thead>
|
|
27
27
|
<tbody>
|
|
28
28
|
{users.map((usr: any) => (
|
|
29
|
-
<tr key={usr._id}
|
|
30
|
-
<td
|
|
31
|
-
<td
|
|
32
|
-
<td
|
|
33
|
-
<span
|
|
34
|
-
padding: '2px 6px',
|
|
35
|
-
borderRadius: 4,
|
|
36
|
-
fontSize: '0.85em',
|
|
37
|
-
background: usr.role === 'admin' ? 'rgba(99, 102, 241, 0.2)' : 'rgba(255, 255, 255, 0.05)',
|
|
38
|
-
color: usr.role === 'admin' ? 'var(--primary-hover)' : 'var(--text-muted)'
|
|
39
|
-
}}>
|
|
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' : ''}`}>
|
|
40
34
|
{usr.role || "user"}
|
|
41
35
|
</span>
|
|
42
36
|
</td>
|
|
43
|
-
<td
|
|
37
|
+
<td>
|
|
44
38
|
<select
|
|
45
39
|
value={usr.role || "user"}
|
|
46
40
|
onChange={(e) => onRoleChange(usr._id, e.target.value as any)}
|
|
47
|
-
style={{ padding: '4px 8px', background: 'var(--surface)', border: '1px solid var(--border)', color: 'var(--text)', borderRadius: 4 }}
|
|
48
41
|
>
|
|
49
42
|
<option value="user">User</option>
|
|
50
43
|
<option value="admin">Admin</option>
|
|
@@ -16,11 +16,21 @@ type UseHeadProps = {
|
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
export function useHead({ title, description, image, canonical, lang }: UseHeadProps) {
|
|
19
|
+
// --- OGIMAGE START ---
|
|
20
|
+
const defaultOgImage = typeof window !== "undefined"
|
|
21
|
+
? `${window.location.origin}/og-image.png?path=${encodeURIComponent(window.location.pathname)}`
|
|
22
|
+
: "/og-image.png";
|
|
23
|
+
const resolvedImage = image || defaultOgImage;
|
|
24
|
+
// --- OGIMAGE END ---
|
|
25
|
+
/* --- NO_OGIMAGE START --- */
|
|
26
|
+
const resolvedImage = image || "/images/icon.png";
|
|
27
|
+
/* --- NO_OGIMAGE END --- */
|
|
28
|
+
|
|
19
29
|
// --- SSR START ---
|
|
20
30
|
useHeaders({
|
|
21
31
|
title,
|
|
22
32
|
description,
|
|
23
|
-
image:
|
|
33
|
+
image: resolvedImage,
|
|
24
34
|
canonical: canonical || (typeof window !== "undefined" ? window.location.origin + window.location.pathname : ""),
|
|
25
35
|
lang,
|
|
26
36
|
});
|
package/template/src/styles.css
CHANGED
|
@@ -458,3 +458,210 @@ button {
|
|
|
458
458
|
margin-top: 4px;
|
|
459
459
|
}
|
|
460
460
|
/* --- AUTH END --- */
|
|
461
|
+
|
|
462
|
+
/* Helper & UI Utility Classes */
|
|
463
|
+
.auth-bar {
|
|
464
|
+
display: flex;
|
|
465
|
+
justify-content: flex-end;
|
|
466
|
+
padding: 10px 0;
|
|
467
|
+
border-bottom: 1px solid var(--border);
|
|
468
|
+
margin-bottom: 20px;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.auth-user-info {
|
|
472
|
+
display: flex;
|
|
473
|
+
align-items: center;
|
|
474
|
+
gap: 15px;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.admin-link-wrapper {
|
|
478
|
+
margin-top: 20px;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.not-found {
|
|
482
|
+
padding: 40px;
|
|
483
|
+
text-align: center;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.loading-fallback {
|
|
487
|
+
padding: 40px;
|
|
488
|
+
text-align: center;
|
|
489
|
+
color: var(--text-muted);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.text-error {
|
|
493
|
+
color: #ef4444;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.text-success {
|
|
497
|
+
color: #34d399;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.alert-success {
|
|
501
|
+
background: rgba(16, 185, 129, 0.1);
|
|
502
|
+
border: 1px solid rgba(16, 185, 129, 0.2);
|
|
503
|
+
color: #34d399;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.reset-password-main {
|
|
507
|
+
max-width: 450px;
|
|
508
|
+
margin: 60px auto;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.btn-group {
|
|
512
|
+
display: flex;
|
|
513
|
+
gap: 15px;
|
|
514
|
+
margin-top: 10px;
|
|
515
|
+
|
|
516
|
+
& > * {
|
|
517
|
+
flex: 1;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.btn-center-group {
|
|
522
|
+
display: flex;
|
|
523
|
+
gap: 15px;
|
|
524
|
+
justify-content: center;
|
|
525
|
+
margin-top: 20px;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.tip-box {
|
|
529
|
+
margin-top: 30px;
|
|
530
|
+
font-size: 0.85em;
|
|
531
|
+
color: var(--text-muted);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.admin-tabs {
|
|
535
|
+
display: flex;
|
|
536
|
+
gap: 10px;
|
|
537
|
+
margin: 20px 0;
|
|
538
|
+
border-bottom: 1px solid var(--border);
|
|
539
|
+
padding-bottom: 10px;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.tab-btn {
|
|
543
|
+
padding: 8px 16px;
|
|
544
|
+
background: transparent;
|
|
545
|
+
border: none;
|
|
546
|
+
color: var(--text-muted);
|
|
547
|
+
font-weight: 500;
|
|
548
|
+
cursor: pointer;
|
|
549
|
+
border-radius: 6px;
|
|
550
|
+
transition: background 0.2s, color 0.2s;
|
|
551
|
+
|
|
552
|
+
&:hover {
|
|
553
|
+
color: var(--text);
|
|
554
|
+
background: rgba(255, 255, 255, 0.05);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
&.active {
|
|
558
|
+
background: var(--primary);
|
|
559
|
+
color: #fff;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.admin-card {
|
|
564
|
+
padding: 40px;
|
|
565
|
+
text-align: center;
|
|
566
|
+
border: 1px dashed var(--border);
|
|
567
|
+
border-radius: 8px;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.admin-login-form {
|
|
571
|
+
display: flex;
|
|
572
|
+
flex-direction: column;
|
|
573
|
+
gap: 10px;
|
|
574
|
+
max-width: 300px;
|
|
575
|
+
margin: 20px auto;
|
|
576
|
+
|
|
577
|
+
input {
|
|
578
|
+
padding: 10px;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
button {
|
|
582
|
+
padding: 10px;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.error-list {
|
|
587
|
+
display: flex;
|
|
588
|
+
flex-direction: column;
|
|
589
|
+
gap: 15px;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.error-item {
|
|
593
|
+
padding: 15px;
|
|
594
|
+
background: var(--surface);
|
|
595
|
+
border: 1px solid var(--border);
|
|
596
|
+
border-radius: 6px;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.error-item-header {
|
|
600
|
+
display: flex;
|
|
601
|
+
justify-content: space-between;
|
|
602
|
+
margin-bottom: 10px;
|
|
603
|
+
font-size: 0.9em;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.error-name {
|
|
607
|
+
color: var(--primary);
|
|
608
|
+
font-weight: bold;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.error-message {
|
|
612
|
+
margin: 5px 0;
|
|
613
|
+
font-family: monospace;
|
|
614
|
+
white-space: pre-wrap;
|
|
615
|
+
word-break: break-all;
|
|
616
|
+
color: #f87171;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.error-details {
|
|
620
|
+
margin-top: 10px;
|
|
621
|
+
|
|
622
|
+
summary {
|
|
623
|
+
cursor: pointer;
|
|
624
|
+
color: var(--text-muted);
|
|
625
|
+
font-size: 0.85em;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
pre {
|
|
629
|
+
margin-top: 5px;
|
|
630
|
+
padding: 10px;
|
|
631
|
+
background: #09090b;
|
|
632
|
+
overflow-x: auto;
|
|
633
|
+
font-size: 0.8em;
|
|
634
|
+
color: var(--text-muted);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.table-wrapper {
|
|
639
|
+
overflow-x: auto;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
.role-badge {
|
|
643
|
+
padding: 2px 6px;
|
|
644
|
+
border-radius: 4px;
|
|
645
|
+
font-size: 0.85em;
|
|
646
|
+
background: rgba(255, 255, 255, 0.05);
|
|
647
|
+
color: var(--text-muted);
|
|
648
|
+
|
|
649
|
+
&.admin {
|
|
650
|
+
background: rgba(99, 102, 241, 0.2);
|
|
651
|
+
color: var(--primary-hover);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.admin-table {
|
|
656
|
+
width: 100%;
|
|
657
|
+
border-collapse: collapse;
|
|
658
|
+
text-align: left;
|
|
659
|
+
|
|
660
|
+
tr {
|
|
661
|
+
border-bottom: 1px solid var(--border);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
th, td {
|
|
665
|
+
padding: 12px 16px;
|
|
666
|
+
}
|
|
667
|
+
}
|
package/template/tsconfig.json
CHANGED
package/template/types.ts
CHANGED
|
@@ -28,16 +28,16 @@ export type API = APIDefinition<{
|
|
|
28
28
|
addItem: Endpoint<Omit<Item, "_id">, string>;
|
|
29
29
|
removeItem: Endpoint<{ id: string }, boolean>;
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
/* --- ADMIN_NO_AUTH START --- */
|
|
32
32
|
getEasyAnalyticsData: Endpoint<{ password: string, month: string }, any[]>;
|
|
33
|
-
|
|
33
|
+
/* --- ADMIN_NO_AUTH END --- */
|
|
34
34
|
// --- ADMIN_WITH_AUTH START ---
|
|
35
35
|
getEasyAnalyticsData: Endpoint<{ month: string }, any[]>;
|
|
36
36
|
// --- ADMIN_WITH_AUTH END ---
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
/* --- ADMIN_ERRORS_NO_AUTH START --- */
|
|
39
39
|
getEasyAnalyticsErrors: Endpoint<{ password: string, month: string }, any[]>;
|
|
40
|
-
|
|
40
|
+
/* --- ADMIN_ERRORS_NO_AUTH END --- */
|
|
41
41
|
// --- ADMIN_ERRORS_WITH_AUTH START ---
|
|
42
42
|
getEasyAnalyticsErrors: Endpoint<{ month: string }, any[]>;
|
|
43
43
|
// --- ADMIN_ERRORS_WITH_AUTH END ---
|
package/template/vite.config.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { defineConfig } from "vite";
|
|
2
2
|
import react from "@vitejs/plugin-react";
|
|
3
|
-
import { imagetools } from "vite-imagetools";
|
|
4
3
|
|
|
5
4
|
export default defineConfig({
|
|
6
|
-
plugins: [react()
|
|
5
|
+
plugins: [react()],
|
|
7
6
|
define: {
|
|
8
7
|
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"),
|
|
9
8
|
"global": "globalThis",
|
|
@@ -19,6 +18,12 @@ export default defineConfig({
|
|
|
19
18
|
target: "http://localhost:1111",
|
|
20
19
|
changeOrigin: true,
|
|
21
20
|
},
|
|
21
|
+
// --- RESIZER START ---
|
|
22
|
+
"/images": {
|
|
23
|
+
target: "http://localhost:1111",
|
|
24
|
+
changeOrigin: true,
|
|
25
|
+
},
|
|
26
|
+
// --- RESIZER END ---
|
|
22
27
|
},
|
|
23
28
|
},
|
|
24
29
|
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|