easy-starter 1.0.0 → 1.2.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 CHANGED
@@ -33,8 +33,74 @@ The generated boilerplate comes pre-configured with a stack designed for develop
33
33
  * **Database:** [`easy-db-node`](https://www.npmjs.com/package/easy-db-node) - A simple, local JSON-based database perfect for small to medium projects where setting up Postgres/MongoDB is overkill.
34
34
  * **Routing:** [`easy-page-router`](https://www.npmjs.com/package/easy-page-router) - A lightweight, programmatic routing solution.
35
35
  * **Analytics:** [`easy-analytics`](https://www.npmjs.com/package/easy-analytics) - Privacy-friendly, local analytics tracking.
36
+ * **AI-Friendly (.cursorrules):** Pre-configured, LLM-optimized guidelines in the project root so AI coding assistants (Cursor, Windsurf, Copilot, etc.) immediately understand the codebase conventions, API restrictions, routing patterns, and database operations.
36
37
  * **Deployment:** Integrated Github Actions workflow and PM2 deployment script for seamless, automated deployment straight to your own VPS.
37
38
 
39
+ ## Core Concepts & Examples
40
+
41
+ > **Note:** `easy-starter` gives you a solid foundation, but you are not forced into any of these patterns. If you prefer a different routing library (like `react-router`), another data fetching solution (like `react-query` or `swr`), or standard REST endpoints, you are completely free to swap them out and use your own libraries!
42
+
43
+ ### 1. Defining your API
44
+
45
+ Your API contract is defined in a single file (`types.d.ts`). This ensures your frontend and backend are always perfectly in sync.
46
+
47
+ ```typescript
48
+ // types.d.ts
49
+ import { APIDefinition, Endpoint } from "typed-client-server-api";
50
+
51
+ export type API = APIDefinition<{
52
+ // Define an endpoint: parameters -> response
53
+ getItems: Endpoint<{}, Item[]>;
54
+ addItem: Endpoint<{ name: string; value: number }, string>;
55
+ }>;
56
+ ```
57
+
58
+ On the server, you implement these endpoints:
59
+
60
+ ```typescript
61
+ // server/index.tsx
62
+ import { initApi } from "typed-client-server-api";
63
+ import { API } from "../types";
64
+
65
+ initApi<API>(app, {
66
+ getItems: async () => {
67
+ return db.select("item");
68
+ },
69
+ addItem: async ({ name, value }) => {
70
+ return db.insert("item", { name, value });
71
+ }
72
+ });
73
+ ```
74
+
75
+ ### 2. Fetching Data with SSR (`useSSRApi`)
76
+
77
+ To fetch data on the server during SSR and seamlessly hydrate it on the client without loading spinners, use `useSSRApi` in your React components.
78
+
79
+ ```tsx
80
+ // src/pages/Index.tsx
81
+ import React from "react";
82
+ import { useSSRApi } from "../services/api";
83
+
84
+ export default function Index() {
85
+ // This will run on the server, fetch the data, embed it in the HTML,
86
+ // and hydrate perfectly on the client. Fully type-safe!
87
+ const [items, error, isLoading, reload] = useSSRApi.getItems({});
88
+
89
+ if (isLoading) return <p>Loading...</p>;
90
+ if (error) return <p>Error: {error.message}</p>;
91
+
92
+ return (
93
+ <ul>
94
+ {items?.map(item => <li key={item._id}>{item.name}</li>)}
95
+ </ul>
96
+ );
97
+ }
98
+ ```
99
+
100
+ ### 3. AI Integration (`.cursorrules`)
101
+
102
+ This template includes a pre-configured `.cursorrules` file in the root. If you develop using AI assistants such as Cursor, Windsurf, or GitHub Copilot, they will automatically read this file to understand the specific conventions of `easy-starter`.
103
+
38
104
  ## Getting Started
39
105
 
40
106
  Bootstrapping a new project is as simple as running one command:
package/bin/index.js CHANGED
@@ -71,6 +71,9 @@ async function main() {
71
71
  if (fs.existsSync(path.join(targetPath, 'env'))) {
72
72
  fs.renameSync(path.join(targetPath, 'env'), path.join(targetPath, '.env'));
73
73
  }
74
+ if (fs.existsSync(path.join(targetPath, 'gitignore'))) {
75
+ fs.renameSync(path.join(targetPath, 'gitignore'), path.join(targetPath, '.gitignore'));
76
+ }
74
77
 
75
78
  // Update site.webmanifest
76
79
  const manifestPath = path.join(targetPath, 'src', 'site.webmanifest');
@@ -98,6 +101,7 @@ async function main() {
98
101
 
99
102
  stripAdminBlocks(path.join(targetPath, 'src', 'Router.tsx'));
100
103
  stripAdminBlocks(path.join(targetPath, 'server', 'index.tsx'));
104
+ stripAdminBlocks(path.join(targetPath, 'src', 'styles.scss'));
101
105
  }
102
106
 
103
107
  // Update package.json from template
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easy-starter",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "A lightweight CLI tool to bootstrap modern fullstack React web projects.",
5
5
  "bin": {
6
6
  "easy-starter": "./bin/index.js"
@@ -15,7 +15,10 @@
15
15
  "typescript",
16
16
  "fullstack",
17
17
  "easy-db",
18
- "parcel"
18
+ "parcel",
19
+ "ai",
20
+ "cursorrules",
21
+ "ai-friendly"
19
22
  ],
20
23
  "author": "Filip Paulů <ing.fenix@seznam.cz>",
21
24
  "license": "MIT",
@@ -0,0 +1,92 @@
1
+ # AI Coding Assistant Guidelines & Project Rules
2
+
3
+ You are an expert AI software engineer pair-programming in a fullstack React application bootstrapped with `easy-starter`.
4
+ Follow these strict rules, project structures, and conventions at all times.
5
+
6
+ ---
7
+
8
+ ## 1. API Architecture & Type Safety (`typed-client-server-api`)
9
+
10
+ All React-to-Express communication must use the RPC contract defined in `types.ts` via the `API` type (`APIDefinition`).
11
+
12
+ ### Naming Conventions for Endpoints
13
+ EVERY endpoint name in `API` **MUST** start with exactly one of the following prefixes:
14
+ * `add...` (Create/Insert operations. Example: `addItem`, `addRecord`)
15
+ * `get...` (Read/Query operations. Example: `getItems`, `getItem`, `getAnalytics`)
16
+ * `update...` (Update/Edit operations. Example: `updateItem`, `updateUser`)
17
+ * `remove...` (Delete/Remove operations. Example: `removeItem`, `removePost`)
18
+
19
+ **CRITICAL**: Do NOT use other prefixes (e.g., `create`, `delete`, `post`, `fetch`, `save`).
20
+
21
+ ### Frontend API Invocation Methods
22
+ You have exactly three (3) methods to trigger API endpoints on the frontend. Use them strictly according to these boundaries:
23
+
24
+ | Method | Import | Use Case | Pattern / Example |
25
+ | :--- | :--- | :--- | :--- |
26
+ | **`useSSRApi`** | `import { useSSRApi } from "../services/api";` | **ONLY** inside React components during initial SSR render. Enables server-side rendering and hydration without flash-of-loading or spinners. | `const [items, error, isLoading, reload] = useSSRApi.getItems({});` |
27
+ | **`useApi`** | `import { useApi } from "../services/api";` | **ONLY** inside React components for client-side-only reactive data fetching (e.g., late loaded components). | `const [items, error, isLoading, reload] = useApi.getItems({});` |
28
+ | **`api`** | `import { api } from "../services/api";` | **ONLY** inside event handlers (e.g., `onClick`, `onSubmit`), utility/non-React functions, or async triggers. Returns a Promise resolving to `[data, error]`. | `const [newId, err] = await api.addItem({ name: "Foo" });` |
29
+
30
+ **NEVER** use standard `fetch`, `axios`, or any other fetcher libraries for internal API calls.
31
+
32
+ ---
33
+
34
+ ## 2. Routing & Navigation (`easy-page-router`)
35
+
36
+ This project uses `easy-page-router`, a lightweight, universal routing library.
37
+
38
+ ### Route Configurations
39
+ * Router paths are defined in `src/Router.tsx` within the `renderPage({ path })` callback of the `Router` component.
40
+ * The `path` parameter is a string array (`string[]`) of URL segments:
41
+ * `/` -> `path.length === 0`
42
+ * `/admin` -> `path[0] === "admin"`
43
+ * `/users/123` -> `path[0] === "users" && path[1]` (where `path[1]` is the user ID segment).
44
+
45
+ ### Navigation
46
+ 1. **Declarative Navigation**: Use `<Link href="/path">Text</Link>` imported from `"easy-page-router/react"`.
47
+ 2. **Programmatic Navigation**: Destructure `push` from the `useRouter()` hook imported from `"easy-page-router/react"`.
48
+
49
+ ```tsx
50
+ import { Link, useRouter } from "easy-page-router/react";
51
+
52
+ // Declarative:
53
+ <Link href="/admin">Go to Admin</Link>
54
+
55
+ // Programmatic:
56
+ const { push } = useRouter();
57
+ const onSubmit = async () => {
58
+ await saveSomething();
59
+ push("/dashboard");
60
+ };
61
+ ```
62
+
63
+ ---
64
+
65
+ ## 3. Database Operations (`easy-db-node`)
66
+
67
+ The local database stores records as JSON files.
68
+
69
+ ### Schema Definition
70
+ * The database collections/tables must be structured and typed in `types.ts` inside the exported `Database` type.
71
+ ```typescript
72
+ export type Database = {
73
+ item: Item;
74
+ user: User;
75
+ };
76
+ ```
77
+
78
+ ### Writing to Database
79
+ * **Auto-generated ID (Standard Insertion)**:
80
+ * Use `insert(table, data)`.
81
+ * **Rule**: Do **NOT** pass `_id` in the input data. `easy-db-node` ignores any `_id` property passed to `insert()` and automatically generates and returns the unique string ID.
82
+ ```typescript
83
+ // CORRECT:
84
+ const newId = await insert("item", { name: "Hello", value: 10 });
85
+ ```
86
+
87
+ * **Custom / Specific ID (Explicit Key)**:
88
+ * To insert or overwrite a record with a **specific/predefined ID**, you **MUST** use `update(table, id, data)` instead of `insert`.
89
+ ```typescript
90
+ // CORRECT (custom ID):
91
+ await update("item", "explicit-custom-id", { name: "Custom ID Item", value: 99 });
92
+ ```
@@ -22,6 +22,71 @@ npm run dev
22
22
 
23
23
  This will concurrently start your Express server and the Parcel bundler. Open [http://localhost:1234](http://localhost:1234) in your browser. The page will automatically reload when you make changes.
24
24
 
25
+ ## Core Concepts & Examples
26
+
27
+ > **Note:** `easy-starter` gives you a solid foundation, but you are not forced into any of these patterns. If you prefer a different routing library (like `react-router`), another data fetching solution (like `react-query` or `swr`), or standard REST endpoints, you are completely free to swap them out and use your own libraries!
28
+
29
+ ### 1. Defining your API
30
+
31
+ Your API contract is defined in a single file (`types.d.ts`). This ensures your frontend and backend are always perfectly in sync.
32
+
33
+ ```typescript
34
+ // types.d.ts
35
+ import { APIDefinition, Endpoint } from "typed-client-server-api";
36
+
37
+ export type API = APIDefinition<{
38
+ // Define an endpoint: parameters -> response
39
+ getItems: Endpoint<{}, Item[]>;
40
+ addItem: Endpoint<{ name: string; value: number }, string>;
41
+ }>;
42
+ ```
43
+
44
+ On the server, you implement these endpoints:
45
+
46
+ ```typescript
47
+ // server/index.tsx
48
+ import { initApi } from "typed-client-server-api";
49
+ import { API } from "../types";
50
+
51
+ initApi<API>(app, {
52
+ getItems: async () => {
53
+ return db.select("item");
54
+ },
55
+ addItem: async ({ name, value }) => {
56
+ return db.insert("item", { name, value });
57
+ }
58
+ });
59
+ ```
60
+
61
+ ### 2. Fetching Data with SSR (`useSSRApi`)
62
+
63
+ To fetch data on the server during SSR and seamlessly hydrate it on the client without loading spinners, use `useSSRApi` in your React components.
64
+
65
+ ```tsx
66
+ // src/pages/Index.tsx
67
+ import React from "react";
68
+ import { useSSRApi } from "../services/api";
69
+
70
+ export default function Index() {
71
+ // This will run on the server, fetch the data, embed it in the HTML,
72
+ // and hydrate perfectly on the client. Fully type-safe!
73
+ const [items, error, isLoading, reload] = useSSRApi.getItems({});
74
+
75
+ if (isLoading) return <p>Loading...</p>;
76
+ if (error) return <p>Error: {error.message}</p>;
77
+
78
+ return (
79
+ <ul>
80
+ {items?.map(item => <li key={item._id}>{item.name}</li>)}
81
+ </ul>
82
+ );
83
+ }
84
+ ```
85
+
86
+ ### 3. AI Integration (`.cursorrules`)
87
+
88
+ This template includes a pre-configured `.cursorrules` file in the root. If you develop using AI assistants such as Cursor, Windsurf, or GitHub Copilot, they will automatically read this file to understand the specific conventions of `easy-starter`:
89
+
25
90
  ## Scripts
26
91
 
27
92
  * `npm run dev` - Starts the development server.
@@ -33,7 +98,8 @@ This will concurrently start your Express server and the Parcel bundler. Open [h
33
98
 
34
99
  * `/src` - Your React frontend code.
35
100
  * `/server` - Your Express backend code.
36
- * `/types.d.ts` - Shared TypeScript definitions for your API and Database.
101
+ * `/types.ts` - Shared TypeScript definitions for your API and Database.
102
+ * `/.cursorrules` - Rules instructing AI coding assistants how to interact with the project stack.
37
103
  * `/public` - Static assets (images, fonts, etc.).
38
104
  * `/easy-db` - Where your local database JSON files are stored.
39
105
 
package/template/env CHANGED
@@ -1,7 +1,10 @@
1
- NODE_ENV=development
1
+ NODE_ENV=development # development/production
2
2
  SERVER_PORT=1111
3
3
  ADMIN_PASSWORD=admin
4
4
 
5
- SSH_HOST=***
5
+ # deploy
6
+ SSH_HOST=[IP_ADDRESS]
6
7
  SSH_USERNAME=root
7
- SSH_USERNAME=***
8
+ SSH_PASSWORD=[PASSWORD]
9
+ SERVER_URL=https://your-production-url.com/
10
+ PM2_APP_NAME=easy-starter-app
@@ -0,0 +1,9 @@
1
+ .parcel-cache/
2
+ node_modules/
3
+ dist/
4
+ easy-db/
5
+ easy-db-files/
6
+ easy-db-backup/
7
+ .DS_Store
8
+ .env
9
+ package-lock.json
@@ -19,12 +19,9 @@
19
19
  "react": "^19.2.6",
20
20
  "react-dom": "^19.2.6",
21
21
  "ssr-hook": "^0.2.0",
22
- "typed-client-server-api": "^1.0.3"
22
+ "typed-client-server-api": "^1.1.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@parcel/packager-raw-url": "^2.16.4",
26
- "@parcel/transformer-sass": "^2.16.4",
27
- "@parcel/transformer-webmanifest": "^2.16.4",
28
25
  "@types/cors": "^2.8.19",
29
26
  "@types/express": "^5.0.6",
30
27
  "@types/node": "^24.0.0",
@@ -32,7 +29,6 @@
32
29
  "@types/react-dom": "^19.2.3",
33
30
  "concurrently": "^9.2.1",
34
31
  "parcel": "^2.16.4",
35
- "sharp": "^0.33.5",
36
32
  "tsx": "^4.21.0",
37
33
  "typescript": "^6.0.3"
38
34
  }
Binary file
@@ -9,16 +9,21 @@ const username = process.env.SSH_USERNAME;
9
9
  const password = process.env.SSH_PASSWORD;
10
10
 
11
11
  // Configuration for your target server
12
- const server = "https://your-production-url.com/";
13
- const pm2AppName = "easy-starter-app"; // The name of the process in PM2
14
- const sshPath = resolve("/", "root", "your-production-url.com"); // Path on the remote VPS
12
+ const server = process.env.SERVER_URL;
13
+ const pm2AppName = process.env.PM2_APP_NAME;
14
+ const sshPath = resolve("/", "root", pm2AppName || ""); // Path on the remote VPS
15
15
 
16
16
  const path = resolve(process.cwd()); // Local project path
17
17
  const ignoreFiles = [
18
18
  ".git",
19
+ ".github",
20
+ ".parcel-cache",
19
21
  "node_modules",
20
22
  "scripts",
21
- ".github"
23
+ "easy-db",
24
+ "easy-db-files",
25
+ "easy-db-backup",
26
+ ".DS_Store",
22
27
  ];
23
28
 
24
29
  // Ensure credentials are provided
@@ -73,20 +78,28 @@ async function deploy() {
73
78
  console.log(`✔ Build project.\n`);
74
79
 
75
80
  // PM2
76
- try {
77
- await sshExec(ssh, `pm2 reload ${pm2AppName}`);
78
- } catch (e) {
79
- console.log(`✘ ${pm2AppName} is not running in PM2!`);
80
- await sshExec(ssh, `pm2 start npm --name "${pm2AppName}" -- start`, `${pm2AppName} could not be registered in PM2!`);
81
+ if (pm2AppName) {
82
+ try {
83
+ await sshExec(ssh, `pm2 reload ${pm2AppName}`);
84
+ } catch (e) {
85
+ console.log(`✘ ${pm2AppName} is not running in PM2!`);
86
+ await sshExec(ssh, `pm2 start npm --name "${pm2AppName}" -- start`, `${pm2AppName} could not be registered in PM2!`);
87
+ }
88
+ await sshExec(ssh, `pm2 list`);
89
+ } else {
90
+ console.log(`i PM2 step is skipped because PM2_APP_NAME is not defined.\n`);
81
91
  }
82
- await sshExec(ssh, `pm2 list`);
83
92
 
84
93
  ssh.dispose();
85
94
  console.log(`✔ SSH connection is closed.\n`);
86
95
 
87
- if (!await serverCheck(server))
88
- throw new Error(`Server ${server} is not available!`);
89
- console.log(`✔ Server ${server} is running.\n`);
96
+ if (server) {
97
+ if (!await serverCheck(server))
98
+ throw new Error(`Server ${server} is not available!`);
99
+ console.log(`✔ Server ${server} is running.\n`);
100
+ } else {
101
+ console.log(`i Server check is skipped because SERVER_URL is not defined.\n`);
102
+ }
90
103
 
91
104
  console.log(`i Server was ${(new Date().getTime() - timeServerOff) / 1000}s off!\n`);
92
105
 
@@ -1,7 +1,8 @@
1
1
  import { resolve } from "path";
2
2
  import { readFile } from "fs/promises";
3
- import express from "express";
3
+
4
4
  import cors from "cors";
5
+ import express from "express";
5
6
 
6
7
  // easy-db-node provides a simple local database storing data in JSON files.
7
8
  // It's ideal for small to medium projects where setting up a full SQL/NoSQL DB is overkill.
@@ -19,12 +20,13 @@ import { setAPIBackend } from "typed-client-server-api";
19
20
  import { postEasyAnalytics, getEasyAnalytics } from "easy-analytics/server";
20
21
 
21
22
  import App from "../src/App";
23
+
22
24
  import { API, Database } from "../types";
23
25
 
24
26
  const PORT = process.env.SERVER_PORT || 1111;
25
27
  const LOCALHOST = `http://localhost:${PORT}`;
26
28
 
27
- // The ORIGIN is used by ssr-hook and typed-client-server-api to know where the requests are coming from.
29
+ // The ORIGIN is used for SEO and by ssr-hook to know where the requests are coming from.
28
30
  const ORIGIN = process.env.NODE_ENV === "development"
29
31
  ? LOCALHOST
30
32
  : "https://your-production-url.com"; // TODO: Update to your domain
@@ -47,12 +49,9 @@ if (process.env.NODE_ENV === "development") {
47
49
  }
48
50
 
49
51
  // ---------------------------- SSR for index ----------------------------------
50
- // This route handles the initial load of our application and renders the React tree to HTML.
51
52
  app.get(["/", "/index.html"], async (req, res) => {
52
53
  try {
53
- // We read the HTML template generated by our bundler (Parcel)
54
54
  const indexHtml = await readFile(INDEX_HTML_PATH, "utf-8");
55
- // We render our App component into that HTML template.
56
55
  const html = await renderToHTML(ORIGIN, req.url, indexHtml, <App />);
57
56
  res.set("Content-Type", "text/html");
58
57
  res.send(html);
@@ -67,9 +66,10 @@ app.get(["/", "/index.html"], async (req, res) => {
67
66
  app.use(express.static("dist"));
68
67
  // Serve any public assets (like images, fonts)
69
68
  app.use(express.static("public"));
70
- // Serve the database files (e.g. uploaded images or JSON files from easy-db-node)
69
+ // Serve the database files (e.g. uploaded images or PDF files from easy-db-node)
71
70
  app.use("/files", express.static("easy-db-files"));
72
71
 
72
+
73
73
  // --------------------------------- SEO ---------------------------------------
74
74
  // Basic configuration for search engine crawlers.
75
75
  app.get("/robots.txt", (_, res) => {
@@ -83,7 +83,7 @@ app.get("/sitemap.xml", async (_, res) => {
83
83
  // TODO: add all pages
84
84
  const sitemap = [{
85
85
  url: ORIGIN + "/",
86
- lastModified: "2024-01-01",
86
+ lastModified: "2026-01-01",
87
87
  changeFrequency: "weekly",
88
88
  priority: 1,
89
89
  }];
@@ -98,10 +98,11 @@ app.get("/sitemap.xml", async (_, res) => {
98
98
  </url>`).join("\n")}</urlset>`);
99
99
  });
100
100
 
101
- // --------------------------- Easy analytics ----------------------------------
101
+
102
102
  // This parses incoming JSON payloads for the API and analytics.
103
103
  app.use(express.json({ limit: "1MB" }));
104
104
 
105
+ // --------------------------- Easy analytics ----------------------------------
105
106
  // Endpoint to receive analytics data from the client.
106
107
  app.post("/api/easy-analytics", async (req, res) => {
107
108
  const userAgent = req.headers['user-agent'] || "";
@@ -116,12 +117,9 @@ app.post("/api/easy-analytics", async (req, res) => {
116
117
  }
117
118
  });
118
119
 
119
- // --- ADMIN START ---
120
- // --- ADMIN END ---
121
-
122
120
  // --------------------------------- API ---------------------------------------
123
121
  // Set up our type-safe API backend. All functions defined here map directly to the
124
- // API type defined in types.d.ts, and can be called from the frontend using the api() client.
122
+ // API type defined in types.d.ts, and can be called from the frontend using by api., useApi., or useSSRApi.
125
123
  setAPIBackend<API>(app, {
126
124
  async getItems() {
127
125
  return await selectArray("item");
@@ -150,8 +148,8 @@ setAPIBackend<API>(app, {
150
148
  // --- ADMIN END ---
151
149
  });
152
150
 
153
- // --------------------------------- SSR ---------------------------------------
154
151
 
152
+ // --------------------------------- SSR ---------------------------------------
155
153
  app.get(/.*/, async (req, res) => {
156
154
  try {
157
155
  const indexHtml = await readFile(INDEX_HTML_PATH, "utf-8");
@@ -165,6 +163,7 @@ app.get(/.*/, async (req, res) => {
165
163
  }
166
164
  });
167
165
 
166
+
168
167
  // Start listening for incoming requests.
169
168
  app.listen(PORT, () => {
170
169
  console.log(`Server is running on ${LOCALHOST}`);
@@ -1,22 +1,15 @@
1
- // RouterProvider manages the browser history and current route state.
2
- import { RouterProvider } from "easy-page-router";
3
-
4
- // init sets up the easy-analytics client to start tracking page views and custom events.
5
1
  import { init } from "easy-analytics/client";
2
+ import { RouterProvider } from "easy-page-router";
6
3
 
7
4
  import Router from "./Router";
8
5
 
9
- // Initialize analytics. It sends data to our Express backend endpoint.
6
+ // Initialize analytics. It sends data to our backend endpoint.
10
7
  init(process.env.NODE_ENV === "development"
11
8
  ? "http://localhost:1111/api/easy-analytics"
12
9
  : "/api/easy-analytics");
13
10
 
14
11
  export default function App() {
15
- return (
16
- // The RouterProvider must wrap your application router so that
17
- // nested components can react to URL changes.
18
- <RouterProvider>
19
- <Router />
20
- </RouterProvider>
21
- );
12
+ return <RouterProvider>
13
+ <Router />
14
+ </RouterProvider>;
22
15
  }
@@ -3,6 +3,7 @@ import { Router } from "easy-page-router/react";
3
3
  import Index from "./pages/Index";
4
4
  import NotFound from "./pages/NotFound";
5
5
  // --- ADMIN START ---
6
+
6
7
  import { lazy, Suspense } from "react";
7
8
  const Admin = lazy(() => import("./pages/admin/Admin"));
8
9
  // --- ADMIN END ---
@@ -12,10 +13,10 @@ export default function AppRouter() {
12
13
  renderPage={({ path }) => {
13
14
  // The 'path' variable is an array of strings representing the URL segments.
14
15
  // For example, "/users/123" would be ["users", "123"].
15
-
16
+
16
17
  // If the path is empty, we are at the root URL (/).
17
18
  if (path.length === 0) return <Index />;
18
-
19
+
19
20
  // --- ADMIN START ---
20
21
  if (path[0] === 'admin') return (
21
22
  <Suspense fallback={<div style={{ padding: 40, textAlign: 'center' }}>Loading admin...</div>}>
@@ -23,11 +24,11 @@ export default function AppRouter() {
23
24
  </Suspense>
24
25
  );
25
26
  // --- ADMIN END ---
26
-
27
+
27
28
  // You can add more routes here, for example:
28
29
  // if (path[0] === 'about') return <AboutPage />;
29
30
  // if (path[0] === 'users' && path[1]) return <UserPage id={path[1]} />;
30
-
31
+
31
32
  // If no routes match, render a 404 Not Found component.
32
33
  return <NotFound />;
33
34
  }}
@@ -1,8 +1,6 @@
1
1
  import React from "react";
2
2
  import { createRoot, hydrateRoot } from "react-dom/client";
3
3
 
4
- // ssr-hook requires knowing the backend origin to fetch initial state during SSR.
5
- import { setSSROrigin } from "ssr-hook";
6
4
  import { handleError } from "./services/api";
7
5
 
8
6
  import App from "./App";
@@ -10,10 +8,6 @@ import App from "./App";
10
8
  const rootElement = document.getElementById("root") as HTMLElement;
11
9
 
12
10
  if (process.env.NODE_ENV === "development") {
13
- // In development mode, the frontend (Parcel) and backend (Express) run on different ports.
14
- // We explicitly tell ssr-hook where the API backend is located.
15
- setSSROrigin("http://localhost:1111");
16
-
17
11
  // We use createRoot because we often don't want to deal with full SSR hydration quirks
18
12
  // during local frontend development (like Hot Module Replacement causing mismatches).
19
13
  createRoot(rootElement).render(<App />);
@@ -1,18 +1,17 @@
1
1
  import { useState } from "react";
2
- // useSSRHook is a specialized hook that fetches data both on the server (during SSR)
3
- // and on the client. It ensures the HTML generated on the server already contains the data.
4
- import { useSSRHook } from "ssr-hook";
5
-
6
- import { api, handleError } from "../services/api";
7
- import { API, Item } from "../../types";
8
2
  import { Link } from "easy-page-router/react";
9
3
 
4
+ import { api, useSSRApi } from "../services/api";
10
5
  import { useHead } from "../services/common";
11
6
 
7
+ import { Item } from "../../types";
8
+
12
9
  export default function Index() {
13
- useHead("Welcome to easy-starter", "The ultimate foundation for your next project.");
10
+ useHead({ title: "Welcome to easy-starter", description: "The ultimate foundation for your next project." });
14
11
 
15
- const [items, error, loading, reload] = useSSRHook<API["getItems"]["result"]>("/api/items");
12
+ // This will run on the server, fetch the data, embed it in the HTML,
13
+ // and hydrate perfectly on the client. Fully type-safe!
14
+ const [items, error, loading, reload] = useSSRApi.getItems({});
16
15
 
17
16
  const [selectedItem, setSelectedItem] = useState<Item | null>(null);
18
17
  const [selectedLoading, setSelectedLoading] = useState(false);
@@ -26,7 +25,6 @@ export default function Index() {
26
25
  const [item, err] = await api.getItem({ id });
27
26
  setSelectedLoading(false);
28
27
  if (item) setSelectedItem(item);
29
- if (err) handleError(err);
30
28
  }
31
29
 
32
30
  async function handleAdd(e: React.FormEvent) {
@@ -39,28 +37,26 @@ export default function Index() {
39
37
  setNewItemValue("");
40
38
  reload();
41
39
  }
42
- if (err) handleError(err);
43
40
  }
44
41
 
45
42
  async function handleRemove(id: string) {
46
43
  if (!confirm("Are you sure you want to delete this item?")) return;
47
44
  const [success, err] = await api.removeItem({ id });
48
45
  if (success) reload();
49
- if (err) handleError(err);
50
46
  }
51
47
 
52
48
  return (
53
- <div className="container">
54
- <div className="hero">
55
- <div className="app-icon"></div>
56
- <h1>Welcome to easy-starter! 🚀</h1>
49
+ <main>
50
+ <header>
51
+ <figure></figure>
52
+ <h1>Welcome to easy-starter!</h1>
57
53
  <p>The ultimate foundation for your next project.</p>
58
54
  <Link href="/admin">
59
55
  → Go to Admin Dashboard
60
56
  </Link>
61
- </div>
57
+ </header>
62
58
 
63
- <div className="api-test">
59
+ <section>
64
60
  <h2>Items (SSR Hook)</h2>
65
61
  {loading && <p>Loading from server...</p>}
66
62
  {error && <p>Error loading items: {error.message}</p>}
@@ -69,10 +65,10 @@ export default function Index() {
69
65
  {items.map(item => (
70
66
  <li key={item._id}>
71
67
  <span><strong>{item.name}</strong> - {item.value}</span>
72
- <div>
68
+ <nav>
73
69
  <button onClick={() => handleView(item._id!)}>View Details</button>
74
- <button className="btn-danger" onClick={() => handleRemove(item._id!)} style={{ marginLeft: 8 }}>Delete</button>
75
- </div>
70
+ <button className="btn-danger" onClick={() => handleRemove(item._id!)}>Delete</button>
71
+ </nav>
76
72
  </li>
77
73
  ))}
78
74
  </ul>
@@ -81,15 +77,15 @@ export default function Index() {
81
77
 
82
78
  {selectedLoading && <p>Loading item details...</p>}
83
79
  {selectedItem && (
84
- <div className="item-details">
80
+ <article>
85
81
  <h3>Selected Item:</h3>
86
82
  <p>ID: {selectedItem._id}</p>
87
83
  <p>Name: {selectedItem.name}</p>
88
84
  <p>Value: {selectedItem.value}</p>
89
- </div>
85
+ </article>
90
86
  )}
91
87
 
92
- <h3 style={{ marginTop: 20 }}>Add New Item</h3>
88
+ <h3>Add New Item</h3>
93
89
  <form onSubmit={handleAdd}>
94
90
  <input
95
91
  type="text"
@@ -109,7 +105,7 @@ export default function Index() {
109
105
  {adding ? "Adding..." : "Add"}
110
106
  </button>
111
107
  </form>
112
- </div>
113
- </div>
108
+ </section>
109
+ </main>
114
110
  );
115
111
  }
@@ -1,8 +1,9 @@
1
- import { useHead } from "../services/common";
2
1
  import { Link } from "easy-page-router/react";
3
2
 
3
+ import { useHead } from "../services/common";
4
+
4
5
  export default function NotFound() {
5
- useHead("404 Not Found", "Page you are looking for does not exist.");
6
+ useHead({ title: "404 Not Found", description: "Page you are looking for does not exist." });
6
7
 
7
8
  return (
8
9
  <div style={{ padding: "40px", textAlign: "center", fontFamily: "sans-serif" }}>
@@ -1,11 +1,12 @@
1
1
  import React, { useEffect, useState } from "react";
2
2
  import { useGroup, GroupTable, getGroupedCount } from "easy-analytics/react";
3
3
  import { Link } from "easy-page-router/react";
4
+
4
5
  import { useHead } from "../../services/common";
5
6
  import { api, handleError } from "../../services/api";
6
7
 
7
8
  export default function Admin() {
8
- useHead("Analytics Dashboard", "View statistics and analytics for your application.");
9
+ useHead({ title: "Analytics Dashboard", description: "View statistics and analytics for your application." });
9
10
 
10
11
  const [password, setPassword] = useState("");
11
12
  const [loggedIn, setLoggedIn] = useState(false);
@@ -34,7 +35,7 @@ export default function Admin() {
34
35
  setLoggedIn(true);
35
36
  localStorage.setItem("admin_pwd", pwd);
36
37
  } else if (err) {
37
- setError(err.message || "Invalid password or server error");
38
+ setError(err);
38
39
  setLoggedIn(false);
39
40
  localStorage.removeItem("admin_pwd");
40
41
  }
@@ -77,7 +78,7 @@ export default function Admin() {
77
78
  return "Unknown";
78
79
  });
79
80
 
80
- // Simplified UA parser
81
+ // Simplified UA parser - for production consider using a library like 'ua-parser-js'
81
82
  const byBrowserLike = useGroup(records, (r: any) => {
82
83
  const ua = (r.userAgent || "").toLowerCase();
83
84
  if (ua.includes("firefox")) return "Firefox";
@@ -1,10 +1,25 @@
1
1
  import { useEffect } from "react";
2
2
  import { sendError } from "easy-analytics/client";
3
3
  import { setServerUrl, getUseAPIFrontend, getAPIFrontend, setHeaders } from "typed-client-server-api/hooks";
4
+ import { useSSRHook } from "ssr-hook/client";
5
+ import { setSSROrigin } from "ssr-hook";
4
6
 
5
7
  import { API } from "../../types";
6
8
 
7
- setServerUrl(process.env.NODE_ENV === "development" ? "http://localhost:1111" : "");
9
+ export type UseSSRAPIType = {
10
+ [I in keyof API]: (params: API[I]["params"]) =>
11
+ | [response: null, error: null, isLoading: boolean, reload: () => void]
12
+ | [response: API[I]["result"], error: null, isLoading: boolean, reload: () => void]
13
+ | [response: null, error: Error, isLoading: boolean, reload: () => void];
14
+ };
15
+
16
+
17
+ if (process.env.NODE_ENV === "development") {
18
+ // In development mode, the frontend (Parcel) and backend (Express) run on different ports.
19
+ // We explicitly tell ssr-hook where the API backend is located.
20
+ setSSROrigin("http://localhost:1111");
21
+ setServerUrl("http://localhost:1111");
22
+ }
8
23
 
9
24
  let authorization = "";
10
25
 
@@ -19,12 +34,20 @@ export function setAuthorizationHeader(newAuthorization: string) {
19
34
 
20
35
  export const api = getAPIFrontend<API>();
21
36
  export const useApi = getUseAPIFrontend<API>();
37
+ export const useSSRApi = new Proxy({}, {
38
+ get(target, prop: string) {
39
+ return (params: any) => {
40
+ const url = api[prop as keyof typeof api].getUrl(params);
41
+ return useSSRHook(url);
42
+ };
43
+ }
44
+ }) as UseSSRAPIType;
22
45
 
23
- export function handleError(error?: null | string | Error, errorInfo?: any) {
46
+ export function handleError(error?: unknown, errorInfo?: any) {
24
47
  if (!error) return;
25
48
  if (typeof error === "string") error = new Error(error);
26
49
 
27
- if (errorInfo && typeof errorInfo === "object" && typeof errorInfo.componentStack === "string") {
50
+ if (error instanceof Error && errorInfo && typeof errorInfo === "object" && typeof errorInfo.componentStack === "string") {
28
51
  error.stack = errorInfo.componentStack;
29
52
  }
30
53
 
@@ -1,12 +1,21 @@
1
1
  import { useHeaders } from "ssr-hook";
2
2
  import { useTitle } from "easy-page-router/react";
3
3
 
4
- export function useHead(title: string, description: string) {
4
+ type UseHeadProps = {
5
+ title: string;
6
+ description: string;
7
+ image?: string;
8
+ canonical?: string;
9
+ lang?: string;
10
+ };
11
+
12
+ export function useHead({ title, description, image, canonical, lang }: UseHeadProps) {
5
13
  useHeaders({
6
14
  title,
7
15
  description,
8
- image: `/images/icon.png`, // Zde je možné zapojit dynamické generování jako v titimeme
9
- canonical: typeof window !== "undefined" ? window.location.origin + window.location.pathname : "",
16
+ image: image || `/images/icon.png`, // Images must be placed in the public directory to be accessible
17
+ canonical: canonical || (typeof window !== "undefined" ? window.location.origin + window.location.pathname : ""),
18
+ lang,
10
19
  });
11
20
  useTitle(title);
12
21
  }
@@ -3,34 +3,45 @@
3
3
  "short_name": "__NAME__",
4
4
  "description": "Welcome to __NAME__ application.",
5
5
  "start_url": "/",
6
+ "display": "standalone",
7
+ "theme_color": "#6366f1",
8
+ "background_color": "#09090b",
6
9
  "icons": [
7
10
  {
8
11
  "src": "./images/icon.png?width=16&height=16",
9
12
  "sizes": "16x16",
10
- "type": "image/png"
13
+ "type": "image/png",
14
+ "purpose": "maskable"
11
15
  },
12
16
  {
13
17
  "src": "./images/icon.png?width=32&height=32",
14
18
  "sizes": "32x32",
15
- "type": "image/png"
19
+ "type": "image/png",
20
+ "purpose": "maskable"
21
+ },
22
+ {
23
+ "src": "./images/icon.png?width=180&height=180",
24
+ "sizes": "180x180",
25
+ "type": "image/png",
26
+ "purpose": "maskable"
16
27
  },
17
28
  {
18
29
  "src": "./images/icon.png?as=webp&width=192&height=192",
19
30
  "sizes": "192x192",
20
- "type": "image/webp"
31
+ "type": "image/webp",
32
+ "purpose": "maskable"
21
33
  },
22
34
  {
23
35
  "src": "./images/icon.png?width=192&height=192",
24
36
  "sizes": "192x192",
25
- "type": "image/png"
37
+ "type": "image/png",
38
+ "purpose": "maskable"
26
39
  },
27
40
  {
28
- "src": "./images/icon.png?width=180&height=180",
29
- "sizes": "180x180",
30
- "type": "image/png"
41
+ "src": "./images/icon.png?as=webp&width=512&height=512",
42
+ "sizes": "512x512",
43
+ "type": "image/webp",
44
+ "purpose": "maskable"
31
45
  }
32
- ],
33
- "theme_color": "#2563eb",
34
- "background_color": "#ffffff",
35
- "display": "standalone"
36
- }
46
+ ]
47
+ }
@@ -27,7 +27,7 @@ a {
27
27
  }
28
28
  }
29
29
 
30
- .container {
30
+ main {
31
31
  max-width: 800px;
32
32
  margin: 50px auto;
33
33
  padding: 40px;
@@ -35,50 +35,38 @@ a {
35
35
  border-radius: 16px;
36
36
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
37
37
  border: 1px solid $border;
38
+ }
38
39
 
39
- .hero {
40
- text-align: center;
41
- margin-bottom: 40px;
40
+ header {
41
+ text-align: center;
42
+ margin-bottom: 40px;
42
43
 
43
- h1 {
44
- margin: 0 0 10px;
45
- color: $text;
46
- font-size: 2.5rem;
47
- background: linear-gradient(90deg, #818cf8, #c084fc);
48
- -webkit-background-clip: text;
49
- -webkit-text-fill-color: transparent;
50
- }
51
-
52
- p {
53
- color: $text-muted;
54
- font-size: 1.1rem;
55
- }
44
+ h1 {
45
+ color: $primary;
46
+ }
56
47
 
57
- .app-icon {
58
- width: 140px;
59
- height: 140px;
60
- margin: 0 auto 20px;
61
- background-image: url("./images/icon.png?as=webp");
62
- background-size: contain;
63
- background-repeat: no-repeat;
64
- background-position: center;
65
- }
48
+ p {
49
+ color: $text-muted;
50
+ }
51
+
52
+ figure {
53
+ width: 140px;
54
+ height: 140px;
55
+ margin: 0 auto 20px;
56
+ background-image: url("./images/icon.png?as=webp");
57
+ background-size: contain;
58
+ background-repeat: no-repeat;
59
+ background-position: center;
66
60
  }
67
61
  }
68
62
 
69
- .api-test {
63
+ section {
70
64
  margin-top: 40px;
71
65
  padding: 30px;
72
66
  background: rgba(255, 255, 255, 0.02);
73
67
  border-radius: 12px;
74
68
  border: 1px solid $border;
75
69
 
76
- h2,
77
- h3 {
78
- margin-top: 0;
79
- color: #fff;
80
- }
81
-
82
70
  ul {
83
71
  list-style: none;
84
72
  padding: 0;
@@ -98,20 +86,23 @@ a {
98
86
  transform: translateY(-2px);
99
87
  border-color: $primary;
100
88
  }
89
+
90
+ nav {
91
+ display: flex;
92
+ gap: 8px;
93
+ }
101
94
  }
102
95
  }
103
96
 
104
- .item-details {
105
- padding: 20px;
97
+ article {
98
+ padding: 0px 20px 10px;
106
99
  background: $bg;
107
- margin-top: 20px;
108
100
  border-radius: 8px;
109
101
  border: 1px solid $primary;
110
102
  box-shadow: 0 0 15px rgba(99, 102, 241, 0.15);
111
103
 
112
104
  h3 {
113
105
  color: $primary;
114
- margin-bottom: 10px;
115
106
  }
116
107
 
117
108
  p {
@@ -123,7 +114,6 @@ a {
123
114
  form {
124
115
  display: flex;
125
116
  gap: 12px;
126
- margin-top: 20px;
127
117
  flex-wrap: wrap;
128
118
  }
129
119
  }
@@ -160,23 +150,16 @@ button {
160
150
  transform: scale(1.02);
161
151
  }
162
152
 
163
- &:active:not(:disabled) {
164
- transform: scale(0.98);
165
- }
166
-
167
- &:disabled {
168
- opacity: 0.5;
169
- cursor: not-allowed;
170
- }
171
-
172
153
  &.btn-danger {
173
154
  background: #ef4444;
155
+
174
156
  &:hover:not(:disabled) {
175
157
  background: #dc2626;
176
158
  }
177
159
  }
178
160
  }
179
161
 
162
+ // --- ADMIN START ---
180
163
  // --- ADMIN DASHBOARD ---
181
164
  .admin-login {
182
165
  padding: 40px;
@@ -184,12 +167,14 @@ button {
184
167
  margin: 0 auto;
185
168
  text-align: center;
186
169
 
187
- h2 { color: $text; }
188
-
170
+ h2 {
171
+ color: $text;
172
+ }
173
+
189
174
  .back-link {
190
175
  margin-bottom: 20px;
191
176
  }
192
-
177
+
193
178
  form {
194
179
  display: flex;
195
180
  flex-direction: column;
@@ -197,7 +182,6 @@ button {
197
182
  margin-top: 20px;
198
183
  }
199
184
 
200
- input { width: 100%; box-sizing: border-box; }
201
185
  }
202
186
 
203
187
  .admin-container {
@@ -211,8 +195,16 @@ button {
211
195
  align-items: center;
212
196
  margin-bottom: 30px;
213
197
 
214
- h2 { margin: 0; color: $text; }
215
- a { color: $primary; text-decoration: none; font-weight: bold; }
198
+ h2 {
199
+ margin: 0;
200
+ color: $text;
201
+ }
202
+
203
+ a {
204
+ color: $primary;
205
+ text-decoration: none;
206
+ font-weight: bold;
207
+ }
216
208
  }
217
209
 
218
210
  .controls {
@@ -222,12 +214,18 @@ button {
222
214
  margin-bottom: 30px;
223
215
  flex-wrap: wrap;
224
216
 
225
- strong { font-size: 1.1rem; color: $text; }
217
+ strong {
218
+ font-size: 1.1rem;
219
+ color: $text;
220
+ }
226
221
 
227
222
  .btn-logout {
228
223
  margin-left: auto;
229
224
  background: #ef4444;
230
- &:hover { background: #dc2626; }
225
+
226
+ &:hover {
227
+ background: #dc2626;
228
+ }
231
229
  }
232
230
  }
233
231
 
@@ -258,16 +256,26 @@ button {
258
256
  justify-content: flex-end;
259
257
  height: 100%;
260
258
 
261
- .count { font-size: 11px; color: $text; margin-bottom: 4px; font-weight: bold; }
262
- .date { font-size: 10px; margin-top: 8px; color: $text-muted; }
263
-
259
+ .count {
260
+ font-size: 11px;
261
+ color: $text;
262
+ margin-bottom: 4px;
263
+ font-weight: bold;
264
+ }
265
+
266
+ .date {
267
+ font-size: 10px;
268
+ margin-top: 8px;
269
+ color: $text-muted;
270
+ }
271
+
264
272
  .bar {
265
273
  width: 100%;
266
274
  max-width: 24px;
267
275
  border-radius: 4px 4px 0 0;
268
276
  transition: height 0.3s ease;
269
277
  }
270
-
278
+
271
279
  .bar.active {
272
280
  background: $primary;
273
281
  }
@@ -280,4 +288,5 @@ button {
280
288
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
281
289
  gap: 20px;
282
290
  }
283
- }
291
+ }
292
+ // --- ADMIN END ---
@@ -18,6 +18,6 @@
18
18
  "include": [
19
19
  "src/**/*",
20
20
  "server/**/*",
21
- "types.d.ts"
21
+ "types.ts"
22
22
  ]
23
23
  }
@@ -22,5 +22,5 @@ export type Item = {
22
22
  // The Database type defines the structure of your JSON files managed by easy-db-node.
23
23
  // Each property represents a 'table' (or a separate JSON file).
24
24
  export type Database = {
25
- item: Omit<Item, "_id">;
25
+ item: Item;
26
26
  };