easy-starter 1.2.0 → 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 (38) 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 +23 -0
  5. package/template/.github/workflows/deploy.yml +3 -0
  6. package/template/README.md +81 -24
  7. package/template/env +3 -2
  8. package/template/gitignore +1 -1
  9. package/template/index.html +15 -0
  10. package/template/package.json +8 -9
  11. package/template/public/images/icon-16.png +0 -0
  12. package/template/public/images/icon-180.png +0 -0
  13. package/template/public/images/icon-192.png +0 -0
  14. package/template/public/images/icon-32.png +0 -0
  15. package/template/public/images/icon-512.png +0 -0
  16. package/template/{src → public}/site.webmanifest +6 -12
  17. package/template/scripts/deploy.ts +3 -2
  18. package/template/server/index.tsx +248 -61
  19. package/template/server/mail.tsx +120 -0
  20. package/template/src/App.tsx +33 -7
  21. package/template/src/Router.tsx +11 -2
  22. package/template/src/index.tsx +28 -1
  23. package/template/src/pages/Index.tsx +45 -6
  24. package/template/src/pages/ResetPassword.tsx +126 -0
  25. package/template/src/pages/admin/Admin.tsx +360 -85
  26. package/template/src/pages/admin/AnalyticsTab.tsx +71 -0
  27. package/template/src/pages/admin/ErrorsTab.tsx +46 -0
  28. package/template/src/pages/admin/UsersTab.tsx +61 -0
  29. package/template/src/services/api.ts +5 -7
  30. package/template/src/services/common.ts +23 -0
  31. package/template/src/styles.css +460 -0
  32. package/template/tsconfig.json +2 -1
  33. package/template/types.ts +44 -3
  34. package/template/vite.config.ts +24 -0
  35. package/template/.parcelrc +0 -6
  36. package/template/src/images/icon.png +0 -0
  37. package/template/src/index.html +0 -16
  38. package/template/src/styles.scss +0 -292
@@ -4,23 +4,28 @@ import { readFile } from "fs/promises";
4
4
  import cors from "cors";
5
5
  import express from "express";
6
6
 
7
- // easy-db-node provides a simple local database storing data in JSON files.
8
- // It's ideal for small to medium projects where setting up a full SQL/NoSQL DB is overkill.
9
7
  import easyDBNode from "easy-db-node";
8
+ import { setAPIBackend } from "typed-client-server-api";
10
9
 
11
- // ssr-hook allows for simple Server-Side Rendering of React components,
12
- // ensuring SEO friendliness and faster initial page loads.
10
+ // --- SSR START ---
13
11
  import { renderToHTML } from "ssr-hook/server";
12
+ // --- SSR END ---
14
13
 
15
- // typed-client-server-api establishes a type-safe RPC-like API between your React frontend
16
- // and this Express backend, preventing mismatch errors and providing autocomplete.
17
- import { setAPIBackend } from "typed-client-server-api";
18
-
19
- // easy-analytics handles privacy-friendly local analytics collection.
14
+ // --- ANALYTICS START ---
20
15
  import { postEasyAnalytics, getEasyAnalytics } from "easy-analytics/server";
16
+ // --- ANALYTICS END ---
21
17
 
22
- 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 ---
23
23
 
24
+ // --- OGIMAGE START ---
25
+ import puppeteer from "puppeteer";
26
+ // --- OGIMAGE END ---
27
+
28
+ import App from "../src/App";
24
29
  import { API, Database } from "../types";
25
30
 
26
31
  const PORT = process.env.SERVER_PORT || 1111;
@@ -29,7 +34,7 @@ const LOCALHOST = `http://localhost:${PORT}`;
29
34
  // The ORIGIN is used for SEO and by ssr-hook to know where the requests are coming from.
30
35
  const ORIGIN = process.env.NODE_ENV === "development"
31
36
  ? LOCALHOST
32
- : "https://your-production-url.com"; // TODO: Update to your domain
37
+ : process.env.SERVER_URL || LOCALHOST;
33
38
 
34
39
  const INDEX_HTML_PATH = resolve(__dirname, "..", "dist", "index.html");
35
40
 
@@ -45,23 +50,48 @@ const app = express();
45
50
 
46
51
  if (process.env.NODE_ENV === "development") {
47
52
  console.log("Starting development server...");
48
- 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
+ }));
49
62
  }
50
63
 
51
- // ---------------------------- SSR for index ----------------------------------
52
- app.get(["/", "/index.html"], async (req, res) => {
53
- try {
54
- const indexHtml = await readFile(INDEX_HTML_PATH, "utf-8");
55
- const html = await renderToHTML(ORIGIN, req.url, indexHtml, <App />);
56
- res.set("Content-Type", "text/html");
57
- res.send(html);
58
- } catch (e) {
59
- console.error("SSR error:", e);
60
- res.status(500);
61
- res.json({ message: "Internal server error" });
62
- }
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
+ },
63
89
  });
64
90
 
91
+ // Register all user authentication routes (/api/login, /api/register, /api/user, etc.)
92
+ authServer.registerExpressRoutes(app);
93
+ // --- AUTH END ---
94
+
65
95
  // Serve static files generated by the bundler (JS, CSS)
66
96
  app.use(express.static("dist"));
67
97
  // Serve any public assets (like images, fonts)
@@ -69,41 +99,95 @@ app.use(express.static("public"));
69
99
  // Serve the database files (e.g. uploaded images or PDF files from easy-db-node)
70
100
  app.use("/files", express.static("easy-db-files"));
71
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
+ }
72
109
 
73
- // --------------------------------- SEO ---------------------------------------
74
- // Basic configuration for search engine crawlers.
75
- app.get("/robots.txt", (_, res) => {
76
- res.type("text/plain");
77
- res.send(`User-agent: *\nAllow: /\n\nSitemap: ${ORIGIN}/sitemap.xml`);
78
- });
110
+ const image = await select("og-image", path);
111
+ if (image) {
112
+ return res.redirect(302, image.file.url);
113
+ }
79
114
 
80
- // Sitemap helps search engines index your site. You should dynamically generate this
81
- // based on your application's routes and database content.
82
- app.get("/sitemap.xml", async (_, res) => {
83
- // TODO: add all pages
84
- const sitemap = [{
85
- url: ORIGIN + "/",
86
- lastModified: "2026-01-01",
87
- changeFrequency: "weekly",
88
- priority: 1,
89
- }];
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
+ });
90
129
 
91
- res.set("Content-Type", "text/xml");
92
- res.send(`<?xml version="1.0" encoding="UTF-8" ?>
93
- <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>
94
- <loc>${s.url}</loc>
95
- <lastmod>${s.lastModified}</lastmod>
96
- ${s.changeFrequency ? `<changefreq>${s.changeFrequency}</changefreq>` : ""}
97
- <priority>${s.priority}</priority>
98
- </url>`).join("\n")}</urlset>`);
99
- });
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();
100
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);
101
151
 
102
- // This parses incoming JSON payloads for the API and analytics.
103
- 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 ---
104
188
 
105
189
  // --------------------------- Easy analytics ----------------------------------
106
- // Endpoint to receive analytics data from the client.
190
+ // --- ANALYTICS START ---
107
191
  app.post("/api/easy-analytics", async (req, res) => {
108
192
  const userAgent = req.headers['user-agent'] || "";
109
193
  const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || "";
@@ -116,10 +200,9 @@ app.post("/api/easy-analytics", async (req, res) => {
116
200
  res.status(400).json({ message: e.message || "Bad request" });
117
201
  }
118
202
  });
203
+ // --- ANALYTICS END ---
119
204
 
120
205
  // --------------------------------- API ---------------------------------------
121
- // Set up our type-safe API backend. All functions defined here map directly to the
122
- // API type defined in types.d.ts, and can be called from the frontend using by api., useApi., or useSSRApi.
123
206
  setAPIBackend<API>(app, {
124
207
  async getItems() {
125
208
  return await selectArray("item");
@@ -127,7 +210,7 @@ setAPIBackend<API>(app, {
127
210
  async getItem({ id }) {
128
211
  const item = await select("item", id);
129
212
  if (!item) throw new Error("Item not found");
130
- return item; // easy-db-node select returns the object
213
+ return item;
131
214
  },
132
215
  async addItem({ name, value }) {
133
216
  const id = await insert("item", { name, value });
@@ -137,19 +220,119 @@ setAPIBackend<API>(app, {
137
220
  await remove("item", id);
138
221
  return true;
139
222
  },
140
- // --- ADMIN START ---
223
+
224
+ // --- ADMIN_NO_AUTH START ---
141
225
  async getEasyAnalyticsData({ password, month }) {
142
226
  const validPassword = process.env.ADMIN_PASSWORD;
143
227
  if (!validPassword || password !== validPassword) {
144
228
  throw new Error("Invalid password or server error");
145
229
  }
230
+ // --- ANALYTICS START ---
146
231
  return await getEasyAnalytics(month);
147
- }
148
- // --- 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`);
149
314
  });
150
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
+ });
151
333
 
152
334
  // --------------------------------- SSR ---------------------------------------
335
+ // --- SSR START ---
153
336
  app.get(/.*/, async (req, res) => {
154
337
  try {
155
338
  const indexHtml = await readFile(INDEX_HTML_PATH, "utf-8");
@@ -158,11 +341,15 @@ app.get(/.*/, async (req, res) => {
158
341
  res.send(html);
159
342
  } catch (e) {
160
343
  console.error("SSR error:", e);
161
- res.status(500);
162
- res.json({ message: "Internal server error" });
344
+ res.status(500).json({ message: "Internal server error" });
163
345
  }
164
346
  });
165
-
347
+ // --- SSR END ---
348
+ // --- NO_SSR START ---
349
+ app.get(/.*/, async (req, res) => {
350
+ res.sendFile(INDEX_HTML_PATH);
351
+ });
352
+ // --- NO_SSR END ---
166
353
 
167
354
  // Start listening for incoming requests.
168
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 ---