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
@@ -5,13 +5,27 @@ import cors from "cors";
5
5
  import express from "express";
6
6
 
7
7
  import easyDBNode from "easy-db-node";
8
- import { renderToHTML } from "ssr-hook/server";
9
8
  import { setAPIBackend } from "typed-client-server-api";
10
9
 
10
+ // --- SSR START ---
11
+ import { renderToHTML } from "ssr-hook/server";
12
+ // --- SSR END ---
13
+
14
+ // --- ANALYTICS START ---
11
15
  import { postEasyAnalytics, getEasyAnalytics } from "easy-analytics/server";
16
+ // --- ANALYTICS END ---
12
17
 
13
- import App from "../src/App";
18
+ // --- AUTH START ---
19
+ import { EasyLoginServer } from "easy-user-auth/server";
20
+ import { sendMailForgottenPassword } from "./mail";
21
+ import { UserProfile } from "../types";
22
+ // --- AUTH END ---
14
23
 
24
+ // --- OGIMAGE START ---
25
+ import puppeteer from "puppeteer";
26
+ // --- OGIMAGE END ---
27
+
28
+ import App from "../src/App";
15
29
  import { API, Database } from "../types";
16
30
 
17
31
  const PORT = process.env.SERVER_PORT || 1111;
@@ -20,7 +34,7 @@ const LOCALHOST = `http://localhost:${PORT}`;
20
34
  // The ORIGIN is used for SEO and by ssr-hook to know where the requests are coming from.
21
35
  const ORIGIN = process.env.NODE_ENV === "development"
22
36
  ? LOCALHOST
23
- : process.env.SERVER_URL;
37
+ : process.env.SERVER_URL || LOCALHOST;
24
38
 
25
39
  const INDEX_HTML_PATH = resolve(__dirname, "..", "dist", "index.html");
26
40
 
@@ -36,23 +50,48 @@ const app = express();
36
50
 
37
51
  if (process.env.NODE_ENV === "development") {
38
52
  console.log("Starting development server...");
39
- app.use(cors());
53
+ app.use(cors({
54
+ origin: ["http://localhost:5173", "http://localhost:1111"],
55
+ credentials: true
56
+ }));
57
+ } else {
58
+ app.use(cors({
59
+ origin: ORIGIN,
60
+ credentials: true
61
+ }));
40
62
  }
41
63
 
42
- // ---------------------------- SSR for index ----------------------------------
43
- app.get(["/", "/index.html"], async (req, res) => {
44
- try {
45
- const indexHtml = await readFile(INDEX_HTML_PATH, "utf-8");
46
- const html = await renderToHTML(ORIGIN, req.url, indexHtml, <App />);
47
- res.set("Content-Type", "text/html");
48
- res.send(html);
49
- } catch (e) {
50
- console.error("SSR error:", e);
51
- res.status(500);
52
- res.json({ message: "Internal server error" });
53
- }
64
+ // Parse incoming JSON payloads.
65
+ app.use(express.json({ limit: "1MB" }));
66
+
67
+ // --- AUTH START ---
68
+ const authServer = new EasyLoginServer<UserProfile>({
69
+ jwtSecret: process.env.JWT_SECRET || "fallback-secret-key-change-me",
70
+ secureCookies: process.env.NODE_ENV === "production",
71
+ db: {
72
+ insertUser: async (user) => insert("user", user),
73
+ getUserById: async (id) => select("user", id),
74
+ getUserByMail: async (mail) => {
75
+ const users = await selectArray("user");
76
+ return users.find(u => u.mail.toLowerCase() === mail.toLowerCase()) || null;
77
+ },
78
+ getUserByRecoveryToken: async (token) => {
79
+ const users = await selectArray("user");
80
+ return users.find(u => u.passwordRecovery?.token === token) || null;
81
+ },
82
+ updateUser: async (id, user) => {
83
+ await update("user", id, user);
84
+ },
85
+ },
86
+ mailSender: async (lang, mailTo, token) => {
87
+ await sendMailForgottenPassword(lang, mailTo, token, ORIGIN);
88
+ },
54
89
  });
55
90
 
91
+ // Register all user authentication routes (/api/login, /api/register, /api/user, etc.)
92
+ authServer.registerExpressRoutes(app);
93
+ // --- AUTH END ---
94
+
56
95
  // Serve static files generated by the bundler (JS, CSS)
57
96
  app.use(express.static("dist"));
58
97
  // Serve any public assets (like images, fonts)
@@ -60,41 +99,95 @@ app.use(express.static("public"));
60
99
  // Serve the database files (e.g. uploaded images or PDF files from easy-db-node)
61
100
  app.use("/files", express.static("easy-db-files"));
62
101
 
102
+ // ------------------------------ og:image -------------------------------------
103
+ // --- OGIMAGE START ---
104
+ app.get("/og-image.png", async (req, res) => {
105
+ const path = req.query.path as string;
106
+ if (!path) {
107
+ return res.sendFile(resolve(__dirname, "..", "public", "images", "icon.png"));
108
+ }
63
109
 
64
- // --------------------------------- SEO ---------------------------------------
65
- // Basic configuration for search engine crawlers.
66
- app.get("/robots.txt", (_, res) => {
67
- res.type("text/plain");
68
- res.send(`User-agent: *\nAllow: /\n\nSitemap: ${ORIGIN}/sitemap.xml`);
69
- });
110
+ const image = await select("og-image", path);
111
+ if (image) {
112
+ return res.redirect(302, image.file.url);
113
+ }
70
114
 
71
- // Sitemap helps search engines index your site. You should dynamically generate this
72
- // based on your application's routes and database content.
73
- app.get("/sitemap.xml", async (_, res) => {
74
- // TODO: add all pages
75
- const sitemap = [{
76
- url: ORIGIN + "/",
77
- lastModified: "2026-01-01",
78
- changeFrequency: "weekly",
79
- priority: 1,
80
- }];
115
+ try {
116
+ const now = Date.now();
117
+ const browser = await puppeteer.launch({
118
+ headless: true,
119
+ args: [
120
+ '--no-sandbox',
121
+ '--disable-setuid-sandbox',
122
+ '--disable-gpu',
123
+ '--disable-dev-shm-usage',
124
+ '--no-first-run',
125
+ '--no-zygote',
126
+ '--single-process',
127
+ ]
128
+ });
81
129
 
82
- res.set("Content-Type", "text/xml");
83
- res.send(`<?xml version="1.0" encoding="UTF-8" ?>
84
- <urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${sitemap.map(s => `<url>
85
- <loc>${s.url}</loc>
86
- <lastmod>${s.lastModified}</lastmod>
87
- ${s.changeFrequency ? `<changefreq>${s.changeFrequency}</changefreq>` : ""}
88
- <priority>${s.priority}</priority>
89
- </url>`).join("\n")}</urlset>`);
90
- });
130
+ const page = await browser.newPage();
131
+ // --- SSR START ---
132
+ await page.setJavaScriptEnabled(false);
133
+ // --- SSR END ---
134
+ // --- NO_SSR START ---
135
+ await page.setJavaScriptEnabled(true);
136
+ // --- NO_SSR END ---
137
+ await page.setViewport({ width: 600, height: 500 });
138
+
139
+ // Render target page
140
+ await page.goto(`${LOCALHOST}${path}`, { waitUntil: 'networkidle0' });
141
+ const base64 = await page.screenshot({ type: "png", encoding: "base64" });
142
+ await browser.close();
91
143
 
144
+ await update("og-image", path, {
145
+ dateCreated: new Date().toISOString(),
146
+ file: {
147
+ type: "EASY_DB_FILE",
148
+ url: `data:image/png;base64,${base64}`,
149
+ },
150
+ } as any);
92
151
 
93
- // This parses incoming JSON payloads for the API and analytics.
94
- app.use(express.json({ limit: "1MB" }));
152
+ console.log(`Render og:image for ${path} took ${Date.now() - now} ms.`);
153
+
154
+ const img = await select("og-image", path);
155
+ return res.redirect(302, img!.file.url);
156
+
157
+ } catch (e) {
158
+ console.error("og:image rendering error:", e);
159
+ return res.sendFile(resolve(__dirname, "..", "public", "images", "icon.png"));
160
+ }
161
+ });
162
+ // --- OGIMAGE END ---
163
+
164
+ // ---------------------------- SSR for index ----------------------------------
165
+ // --- SSR START ---
166
+ app.get(["/", "/index.html"], async (req, res) => {
167
+ try {
168
+ const indexHtml = await readFile(INDEX_HTML_PATH, "utf-8");
169
+ const html = await renderToHTML(ORIGIN, req.url, indexHtml, <App />);
170
+ res.set("Content-Type", "text/html");
171
+ res.send(html);
172
+ } catch (e) {
173
+ console.error("SSR error:", e);
174
+ res.status(500).json({ message: "Internal server error" });
175
+ }
176
+ });
177
+ // --- SSR END ---
178
+ // --- NO_SSR START ---
179
+ app.get(["/", "/index.html"], async (req, res) => {
180
+ try {
181
+ res.sendFile(INDEX_HTML_PATH);
182
+ } catch (e) {
183
+ console.error("Error serving index.html:", e);
184
+ res.status(500).json({ message: "Internal server error" });
185
+ }
186
+ });
187
+ // --- NO_SSR END ---
95
188
 
96
189
  // --------------------------- Easy analytics ----------------------------------
97
- // Endpoint to receive analytics data from the client.
190
+ // --- ANALYTICS START ---
98
191
  app.post("/api/easy-analytics", async (req, res) => {
99
192
  const userAgent = req.headers['user-agent'] || "";
100
193
  const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || "";
@@ -107,10 +200,9 @@ app.post("/api/easy-analytics", async (req, res) => {
107
200
  res.status(400).json({ message: e.message || "Bad request" });
108
201
  }
109
202
  });
203
+ // --- ANALYTICS END ---
110
204
 
111
205
  // --------------------------------- API ---------------------------------------
112
- // Set up our type-safe API backend. All functions defined here map directly to the
113
- // API type defined in types.d.ts, and can be called from the frontend using by api., useApi., or useSSRApi.
114
206
  setAPIBackend<API>(app, {
115
207
  async getItems() {
116
208
  return await selectArray("item");
@@ -118,7 +210,7 @@ setAPIBackend<API>(app, {
118
210
  async getItem({ id }) {
119
211
  const item = await select("item", id);
120
212
  if (!item) throw new Error("Item not found");
121
- return item; // easy-db-node select returns the object
213
+ return item;
122
214
  },
123
215
  async addItem({ name, value }) {
124
216
  const id = await insert("item", { name, value });
@@ -128,19 +220,119 @@ setAPIBackend<API>(app, {
128
220
  await remove("item", id);
129
221
  return true;
130
222
  },
131
- // --- ADMIN START ---
223
+
224
+ // --- ADMIN_NO_AUTH START ---
132
225
  async getEasyAnalyticsData({ password, month }) {
133
226
  const validPassword = process.env.ADMIN_PASSWORD;
134
227
  if (!validPassword || password !== validPassword) {
135
228
  throw new Error("Invalid password or server error");
136
229
  }
230
+ // --- ANALYTICS START ---
137
231
  return await getEasyAnalytics(month);
138
- }
139
- // --- ADMIN END ---
232
+ // --- ANALYTICS END ---
233
+ // --- NO_ANALYTICS START ---
234
+ return [];
235
+ // --- NO_ANALYTICS END ---
236
+ },
237
+ // --- ADMIN_NO_AUTH END ---
238
+
239
+ // --- ADMIN_WITH_AUTH START ---
240
+ async getEasyAnalyticsData({ month }, req, res) {
241
+ const session = await authServer.checkLogin(req!, res!);
242
+ const user = await select("user", session.userId);
243
+ if (!user || user.role !== "admin") {
244
+ throw new Error("Forbidden");
245
+ }
246
+ // --- ANALYTICS START ---
247
+ return await getEasyAnalytics(month);
248
+ // --- ANALYTICS END ---
249
+ // --- NO_ANALYTICS START ---
250
+ return [];
251
+ // --- NO_ANALYTICS END ---
252
+ },
253
+ // --- ADMIN_WITH_AUTH END ---
254
+
255
+ // --- ADMIN_ERRORS_NO_AUTH START ---
256
+ async getEasyAnalyticsErrors({ password, month }) {
257
+ const validPassword = process.env.ADMIN_PASSWORD;
258
+ if (!validPassword || password !== validPassword) {
259
+ throw new Error("Invalid password or server error");
260
+ }
261
+ return await selectArray(`easyAnalyticsError-${month}`);
262
+ },
263
+ // --- ADMIN_ERRORS_NO_AUTH END ---
264
+
265
+ // --- ADMIN_ERRORS_WITH_AUTH START ---
266
+ async getEasyAnalyticsErrors({ month }, req, res) {
267
+ const session = await authServer.checkLogin(req!, res!);
268
+ const user = await select("user", session.userId);
269
+ if (!user || user.role !== "admin") {
270
+ throw new Error("Forbidden");
271
+ }
272
+ return await selectArray(`easyAnalyticsError-${month}`);
273
+ },
274
+ // --- ADMIN_ERRORS_WITH_AUTH END ---
275
+
276
+ // --- ADMIN_USERS START ---
277
+ async getUsers(params, req, res) {
278
+ const session = await authServer.checkLogin(req!, res!);
279
+ const user = await select("user", session.userId);
280
+ if (!user || user.role !== "admin") {
281
+ throw new Error("Forbidden");
282
+ }
283
+ const allUsers = await selectArray("user");
284
+ return allUsers.map(u => ({
285
+ _id: u._id,
286
+ email: u.mail,
287
+ name: u.name || u.profile?.name || "",
288
+ role: u.profile?.role || u.role || "user"
289
+ }));
290
+ },
291
+ async updateUserRole({ userId, role }, req, res) {
292
+ const session = await authServer.checkLogin(req!, res!);
293
+ const user = await select("user", session.userId);
294
+ if (!user || user.role !== "admin") {
295
+ throw new Error("Forbidden");
296
+ }
297
+ const targetUser = await select("user", userId);
298
+ if (!targetUser) {
299
+ throw new Error("User not found");
300
+ }
301
+ targetUser.role = role;
302
+ targetUser.profile = targetUser.profile || { name: targetUser.name || "", role: "user" };
303
+ targetUser.profile.role = role;
304
+ await update("user", userId, targetUser);
305
+ return true;
306
+ },
307
+ // --- ADMIN_USERS END ---
308
+ });
309
+
310
+ // --------------------------------- SEO ---------------------------------------
311
+ app.get("/robots.txt", (_, res) => {
312
+ res.type("text/plain");
313
+ res.send(`User-agent: *\nAllow: /\n\nSitemap: ${ORIGIN}/sitemap.xml`);
140
314
  });
141
315
 
316
+ app.get("/sitemap.xml", async (_, res) => {
317
+ const sitemap = [{
318
+ url: ORIGIN + "/",
319
+ lastModified: new Date().toISOString().slice(0, 10),
320
+ changeFrequency: "weekly",
321
+ priority: 1,
322
+ }];
323
+
324
+ res.set("Content-Type", "text/xml");
325
+ res.send(`<?xml version="1.0" encoding="UTF-8" ?>
326
+ <urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${sitemap.map(s => `<url>
327
+ <loc>${s.url}</loc>
328
+ <lastmod>${s.lastModified}</lastmod>
329
+ ${s.changeFrequency ? `<changefreq>${s.changeFrequency}</changefreq>` : ""}
330
+ <priority>${s.priority}</priority>
331
+ </url>`).join("\n")}</urlset>`);
332
+ });
142
333
 
143
334
  // --------------------------------- SSR ---------------------------------------
335
+ // --- SSR START ---
144
336
  app.get(/.*/, async (req, res) => {
145
337
  try {
146
338
  const indexHtml = await readFile(INDEX_HTML_PATH, "utf-8");
@@ -149,11 +341,15 @@ app.get(/.*/, async (req, res) => {
149
341
  res.send(html);
150
342
  } catch (e) {
151
343
  console.error("SSR error:", e);
152
- res.status(500);
153
- res.json({ message: "Internal server error" });
344
+ res.status(500).json({ message: "Internal server error" });
154
345
  }
155
346
  });
156
-
347
+ // --- SSR END ---
348
+ // --- NO_SSR START ---
349
+ app.get(/.*/, async (req, res) => {
350
+ res.sendFile(INDEX_HTML_PATH);
351
+ });
352
+ // --- NO_SSR END ---
157
353
 
158
354
  // Start listening for incoming requests.
159
355
  app.listen(PORT, () => {
@@ -0,0 +1,120 @@
1
+ import nodemailer from "nodemailer";
2
+ import React from "react";
3
+ import { renderToString } from "react-dom/server";
4
+
5
+ const mailHost = process.env.MAIL_SMTP;
6
+ const mailUser = process.env.MAIL_USER;
7
+ const mailPort = Number(process.env.MAIL_PORT || 587);
8
+ const mailPass = process.env.MAIL_PASSWORD;
9
+
10
+ const transporter = (mailHost && mailUser && mailPass) ? nodemailer.createTransport({
11
+ host: mailHost,
12
+ port: mailPort,
13
+ secure: mailPort === 465,
14
+ auth: {
15
+ user: mailUser,
16
+ pass: mailPass,
17
+ },
18
+ }) : null;
19
+
20
+ async function sendMail(mailTo: string, subject: string, html: string) {
21
+ if (!transporter) {
22
+ console.warn("\n⚠️ SMTP is not configured. Email was not sent.");
23
+ console.log(`To: ${mailTo}`);
24
+ console.log(`Subject: ${subject}`);
25
+ console.log(`Content HTML preview:\n${html}\n`);
26
+ return true; // Simulate success in dev if not configured
27
+ }
28
+
29
+ try {
30
+ const info = await transporter.sendMail({
31
+ from: `"My App" <${mailUser}>`,
32
+ to: mailTo,
33
+ subject,
34
+ html,
35
+ });
36
+
37
+ return info.accepted.includes(mailTo);
38
+ } catch (err) {
39
+ console.error("Error sending email:", err);
40
+ return false;
41
+ }
42
+ }
43
+
44
+ const mailTranslations: Record<string, {
45
+ subject: string;
46
+ title: string;
47
+ instruction: string;
48
+ button: string;
49
+ fallback: string;
50
+ disclaimer: string;
51
+ signoff: string;
52
+ }> = {
53
+ cs: {
54
+ subject: "Zapomenuté heslo",
55
+ title: "Zapomenuté heslo",
56
+ instruction: "Pro nastavení nového hesla klikněte na následující tlačítko:",
57
+ button: "Změnit heslo",
58
+ fallback: "Pokud tlačítko nefunguje, zkopírujte tento odkaz do prohlížeče:",
59
+ disclaimer: "Pokud jste o změnu hesla nežádali, tento e-mail prosím ignorujte.",
60
+ signoff: "S pozdravem,",
61
+ },
62
+ en: {
63
+ subject: "Forgotten Password",
64
+ title: "Forgotten Password",
65
+ instruction: "To set your new password, click the following button:",
66
+ button: "Change password",
67
+ fallback: "If the button does not work, try copying this link into your browser:",
68
+ disclaimer: "If you did not request a password change, please ignore this email.",
69
+ signoff: "Kind regards,",
70
+ },
71
+ es: {
72
+ subject: "Contraseña olvidada",
73
+ title: "Contraseña olvidada",
74
+ instruction: "Para configurar tu nueva contraseña, haz clic en el siguiente botón:",
75
+ button: "Cambiar contraseña",
76
+ fallback: "Si el botón no funciona, prueba copiando este enlace en tu navegador:",
77
+ disclaimer: "Si no solicitaste un cambio de contraseña, ignora este correo electrónico.",
78
+ signoff: "Saludos cordiales,",
79
+ }
80
+ };
81
+
82
+ export async function sendMailForgottenPassword(lang: string, mailTo: string, token: string, origin: string) {
83
+ const link = `${origin}/reset-password?token=${encodeURIComponent(token)}`;
84
+ const t = mailTranslations[lang] || mailTranslations.en;
85
+
86
+ const isMailSent = await sendMail(mailTo, t.subject, renderToString(
87
+ <div style={{ fontFamily: "sans-serif", padding: 20, color: "#333" }}>
88
+ <div style={{ backgroundColor: "#333", padding: "20px", color: "#fff", fontSize: "24px", fontWeight: "bold", textAlign: "center", borderRadius: "8px 8px 0 0" }}>
89
+ My App
90
+ </div>
91
+ <div style={{ padding: "20px", border: "1px solid #ddd", borderRadius: "0 0 8px 8px", backgroundColor: "#fafafa" }}>
92
+ <h2>{t.title}</h2>
93
+ <p>{t.instruction}</p>
94
+ <a href={link} target="_blank" rel="noopener noreferrer" style={{
95
+ display: "inline-block",
96
+ margin: "16px 0px",
97
+ padding: "12px 24px",
98
+ borderRadius: "6px",
99
+ backgroundColor: "#6366f1",
100
+ color: "#ffffff",
101
+ textDecoration: "none",
102
+ fontWeight: "bold",
103
+ }}>
104
+ {t.button}
105
+ </a>
106
+ <p style={{ fontSize: "12px", color: "#777" }}>{t.fallback}</p>
107
+ <pre style={{ backgroundColor: "#eee", padding: 10, borderRadius: 4, overflowX: "auto", fontSize: "11px" }}>{link}</pre>
108
+ <p style={{ fontSize: "12px", color: "#999", marginTop: 20 }}>{t.disclaimer}</p>
109
+ <p style={{ borderTop: "1px solid #eee", paddingTop: 15, marginTop: 20 }}>
110
+ {t.signoff}<br />
111
+ <strong>My App Team</strong>
112
+ </p>
113
+ </div>
114
+ </div>
115
+ ));
116
+
117
+ if (!isMailSent) {
118
+ throw new Error("Failed to send forgotten password email.");
119
+ }
120
+ }
@@ -1,15 +1,41 @@
1
- import { init } from "easy-analytics/client";
1
+ import React from "react";
2
2
  import { RouterProvider } from "easy-page-router";
3
3
 
4
4
  import Router from "./Router";
5
5
 
6
+ // --- ANALYTICS START ---
7
+ import { init } from "easy-analytics/client";
8
+
6
9
  // Initialize analytics. It sends data to our backend endpoint.
7
- init(process.env.NODE_ENV === "development"
8
- ? "http://localhost:1111/api/easy-analytics"
9
- : "/api/easy-analytics");
10
+ // In development, Vite proxies this relative path to the Express server.
11
+ init("/api/easy-analytics");
12
+ // --- ANALYTICS END ---
13
+
14
+ // --- AUTH START ---
15
+ import { UserProvider } from "easy-user-auth/react";
16
+ import { UserProfile } from "../types";
17
+ // --- AUTH END ---
10
18
 
11
19
  export default function App() {
12
- return <RouterProvider>
13
- <Router />
14
- </RouterProvider>;
20
+ // --- AUTH START ---
21
+ const defaultUser: UserProfile = {
22
+ name: "",
23
+ role: "user",
24
+ };
25
+
26
+ return (
27
+ <UserProvider<UserProfile> defaultUser={defaultUser}>
28
+ <RouterProvider>
29
+ <Router />
30
+ </RouterProvider>
31
+ </UserProvider>
32
+ );
33
+ // --- AUTH END ---
34
+ // --- NO_AUTH START ---
35
+ return (
36
+ <RouterProvider>
37
+ <Router />
38
+ </RouterProvider>
39
+ );
40
+ // --- NO_AUTH END ---
15
41
  }
@@ -1,13 +1,18 @@
1
+ import React from "react";
1
2
  import { Router } from "easy-page-router/react";
2
3
 
3
4
  import Index from "./pages/Index";
4
5
  import NotFound from "./pages/NotFound";
5
- // --- ADMIN START ---
6
6
 
7
+ // --- ADMIN START ---
7
8
  import { lazy, Suspense } from "react";
8
9
  const Admin = lazy(() => import("./pages/admin/Admin"));
9
10
  // --- ADMIN END ---
10
11
 
12
+ // --- AUTH START ---
13
+ import ResetPassword from "./pages/ResetPassword";
14
+ // --- AUTH END ---
15
+
11
16
  export default function AppRouter() {
12
17
  return <Router
13
18
  renderPage={({ path }) => {
@@ -20,11 +25,15 @@ export default function AppRouter() {
20
25
  // --- ADMIN START ---
21
26
  if (path[0] === 'admin') return (
22
27
  <Suspense fallback={<div style={{ padding: 40, textAlign: 'center' }}>Loading admin...</div>}>
23
- <Admin />
28
+ <Admin path={path} />
24
29
  </Suspense>
25
30
  );
26
31
  // --- ADMIN END ---
27
32
 
33
+ // --- AUTH START ---
34
+ if (path[0] === 'reset-password') return <ResetPassword />;
35
+ // --- AUTH END ---
36
+
28
37
  // You can add more routes here, for example:
29
38
  // if (path[0] === 'about') return <AboutPage />;
30
39
  // if (path[0] === 'users' && path[1]) return <UserPage id={path[1]} />;
@@ -1,12 +1,35 @@
1
1
  import React from "react";
2
- import { createRoot, hydrateRoot } from "react-dom/client";
2
+ import { createRoot } from "react-dom/client";
3
+ // --- SSR START ---
4
+ import { hydrateRoot } from "react-dom/client";
5
+ // --- SSR END ---
3
6
 
4
7
  import { handleError } from "./services/api";
5
8
 
6
9
  import App from "./App";
10
+ import "./styles.css";
11
+
12
+ // --- VIEWPORT START ---
13
+ // Fix iOS safari issues with viewport
14
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
15
+ const updateViewport = () => {
16
+ const metaViewport = document.querySelector('meta[name="viewport"]');
17
+ if (metaViewport) {
18
+ if (window.screen.width <= 600) {
19
+ metaViewport.setAttribute('content', 'width=600, user-scalable=no');
20
+ } else {
21
+ metaViewport.setAttribute('content', 'width=device-width');
22
+ }
23
+ }
24
+ }
25
+ updateViewport();
26
+ window.addEventListener('resize', updateViewport);
27
+ }
28
+ // --- VIEWPORT END ---
7
29
 
8
30
  const rootElement = document.getElementById("root") as HTMLElement;
9
31
 
32
+ // --- SSR START ---
10
33
  if (process.env.NODE_ENV === "development") {
11
34
  // We use createRoot because we often don't want to deal with full SSR hydration quirks
12
35
  // during local frontend development (like Hot Module Replacement causing mismatches).
@@ -21,3 +44,7 @@ if (process.env.NODE_ENV === "development") {
21
44
  onRecoverableError: handleError,
22
45
  });
23
46
  }
47
+ // --- SSR END ---
48
+ // --- NO_SSR START ---
49
+ createRoot(rootElement).render(<App />);
50
+ // --- NO_SSR END ---