easy-starter 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,6 +35,67 @@ The generated boilerplate comes pre-configured with a stack designed for develop
35
35
  * **Analytics:** [`easy-analytics`](https://www.npmjs.com/package/easy-analytics) - Privacy-friendly, local analytics tracking.
36
36
  * **Deployment:** Integrated Github Actions workflow and PM2 deployment script for seamless, automated deployment straight to your own VPS.
37
37
 
38
+ ## Core Concepts & Examples
39
+
40
+ > **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!
41
+
42
+ ### 1. Defining your API
43
+
44
+ Your API contract is defined in a single file (`types.d.ts`). This ensures your frontend and backend are always perfectly in sync.
45
+
46
+ ```typescript
47
+ // types.d.ts
48
+ import { APIDefinition, Endpoint } from "typed-client-server-api";
49
+
50
+ export type API = APIDefinition<{
51
+ // Define an endpoint: parameters -> response
52
+ getItems: Endpoint<{}, Item[]>;
53
+ addItem: Endpoint<{ name: string; value: number }, string>;
54
+ }>;
55
+ ```
56
+
57
+ On the server, you implement these endpoints:
58
+
59
+ ```typescript
60
+ // server/index.tsx
61
+ import { initApi } from "typed-client-server-api";
62
+ import { API } from "../types";
63
+
64
+ initApi<API>(app, {
65
+ getItems: async () => {
66
+ return db.select("item");
67
+ },
68
+ addItem: async ({ name, value }) => {
69
+ return db.insert("item", { name, value });
70
+ }
71
+ });
72
+ ```
73
+
74
+ ### 2. Fetching Data with SSR (`useSSRApi`)
75
+
76
+ To fetch data on the server during SSR and seamlessly hydrate it on the client without loading spinners, use `useSSRApi` in your React components.
77
+
78
+ ```tsx
79
+ // src/pages/Index.tsx
80
+ import React from "react";
81
+ import { useSSRApi } from "../services/api";
82
+
83
+ export default function Index() {
84
+ // This will run on the server, fetch the data, embed it in the HTML,
85
+ // and hydrate perfectly on the client. Fully type-safe!
86
+ const [items, error, isLoading, reload] = useSSRApi.getItems({});
87
+
88
+ if (isLoading) return <p>Loading...</p>;
89
+ if (error) return <p>Error: {error.message}</p>;
90
+
91
+ return (
92
+ <ul>
93
+ {items?.map(item => <li key={item._id}>{item.name}</li>)}
94
+ </ul>
95
+ );
96
+ }
97
+ ```
98
+
38
99
  ## Getting Started
39
100
 
40
101
  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.1.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"
@@ -22,6 +22,67 @@ 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
+
25
86
  ## Scripts
26
87
 
27
88
  * `npm run dev` - Starts the development server.
package/template/env CHANGED
@@ -2,6 +2,7 @@ NODE_ENV=development
2
2
  SERVER_PORT=1111
3
3
  ADMIN_PASSWORD=admin
4
4
 
5
+ # deploy
5
6
  SSH_HOST=***
6
- SSH_USERNAME=root
7
+ SSH_USERNAME=***
7
8
  SSH_USERNAME=***
@@ -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
@@ -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" }}>
@@ -5,7 +5,7 @@ import { useHead } from "../../services/common";
5
5
  import { api, handleError } from "../../services/api";
6
6
 
7
7
  export default function Admin() {
8
- useHead("Analytics Dashboard", "View statistics and analytics for your application.");
8
+ useHead({ title: "Analytics Dashboard", description: "View statistics and analytics for your application." });
9
9
 
10
10
  const [password, setPassword] = useState("");
11
11
  const [loggedIn, setLoggedIn] = useState(false);
@@ -34,7 +34,7 @@ export default function Admin() {
34
34
  setLoggedIn(true);
35
35
  localStorage.setItem("admin_pwd", pwd);
36
36
  } else if (err) {
37
- setError(err.message || "Invalid password or server error");
37
+ setError(err);
38
38
  setLoggedIn(false);
39
39
  localStorage.removeItem("admin_pwd");
40
40
  }
@@ -77,7 +77,7 @@ export default function Admin() {
77
77
  return "Unknown";
78
78
  });
79
79
 
80
- // Simplified UA parser
80
+ // Simplified UA parser - for production consider using a library like 'ua-parser-js'
81
81
  const byBrowserLike = useGroup(records, (r: any) => {
82
82
  const ua = (r.userAgent || "").toLowerCase();
83
83
  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 ---