@finesoft/create-app 0.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.
Files changed (128) hide show
  1. package/dist/index.mjs +129 -0
  2. package/package.json +26 -0
  3. package/templates/react/env.d.ts +1 -0
  4. package/templates/react/index.html +13 -0
  5. package/templates/react/package.json +21 -0
  6. package/templates/react/src/App.tsx +20 -0
  7. package/templates/react/src/actions.ts +14 -0
  8. package/templates/react/src/bootstrap.ts +36 -0
  9. package/templates/react/src/components/Layout.tsx +63 -0
  10. package/templates/react/src/components/Loading.tsx +3 -0
  11. package/templates/react/src/components/Navigation.tsx +51 -0
  12. package/templates/react/src/components/PageRenderer.tsx +41 -0
  13. package/templates/react/src/components/ProductCard.tsx +35 -0
  14. package/templates/react/src/lib/controllers/about.ts +18 -0
  15. package/templates/react/src/lib/controllers/error.ts +11 -0
  16. package/templates/react/src/lib/controllers/home.ts +53 -0
  17. package/templates/react/src/lib/controllers/product-detail.ts +52 -0
  18. package/templates/react/src/lib/controllers/search.ts +58 -0
  19. package/templates/react/src/lib/guards/auth.ts +17 -0
  20. package/templates/react/src/lib/guards/seo.ts +15 -0
  21. package/templates/react/src/lib/mappers/product.ts +27 -0
  22. package/templates/react/src/lib/models/product.ts +48 -0
  23. package/templates/react/src/lib/services/api-client.ts +28 -0
  24. package/templates/react/src/main.tsx +49 -0
  25. package/templates/react/src/pages/About.tsx +16 -0
  26. package/templates/react/src/pages/Home.tsx +34 -0
  27. package/templates/react/src/pages/NotFound.tsx +25 -0
  28. package/templates/react/src/pages/ProductDetail.tsx +34 -0
  29. package/templates/react/src/pages/Search.tsx +23 -0
  30. package/templates/react/src/ssr.tsx +23 -0
  31. package/templates/react/tsconfig.json +17 -0
  32. package/templates/react/vite.config.ts +20 -0
  33. package/templates/react-minimal/env.d.ts +1 -0
  34. package/templates/react-minimal/index.html +13 -0
  35. package/templates/react-minimal/package.json +21 -0
  36. package/templates/react-minimal/src/App.tsx +22 -0
  37. package/templates/react-minimal/src/bootstrap.ts +6 -0
  38. package/templates/react-minimal/src/lib/controllers/home.ts +14 -0
  39. package/templates/react-minimal/src/main.tsx +44 -0
  40. package/templates/react-minimal/src/ssr.tsx +25 -0
  41. package/templates/react-minimal/tsconfig.json +17 -0
  42. package/templates/react-minimal/vite.config.ts +12 -0
  43. package/templates/svelte/env.d.ts +9 -0
  44. package/templates/svelte/index.html +13 -0
  45. package/templates/svelte/package.json +18 -0
  46. package/templates/svelte/src/App.svelte +85 -0
  47. package/templates/svelte/src/actions.ts +14 -0
  48. package/templates/svelte/src/bootstrap.ts +36 -0
  49. package/templates/svelte/src/components/Layout.svelte +19 -0
  50. package/templates/svelte/src/components/Loading.svelte +1 -0
  51. package/templates/svelte/src/components/Navigation.svelte +75 -0
  52. package/templates/svelte/src/components/PageRenderer.svelte +22 -0
  53. package/templates/svelte/src/components/ProductCard.svelte +37 -0
  54. package/templates/svelte/src/lib/controllers/about.ts +18 -0
  55. package/templates/svelte/src/lib/controllers/error.ts +11 -0
  56. package/templates/svelte/src/lib/controllers/home.ts +53 -0
  57. package/templates/svelte/src/lib/controllers/product-detail.ts +52 -0
  58. package/templates/svelte/src/lib/controllers/search.ts +58 -0
  59. package/templates/svelte/src/lib/framework-svelte.ts +34 -0
  60. package/templates/svelte/src/lib/guards/auth.ts +17 -0
  61. package/templates/svelte/src/lib/guards/seo.ts +15 -0
  62. package/templates/svelte/src/lib/mappers/product.ts +27 -0
  63. package/templates/svelte/src/lib/models/product.ts +47 -0
  64. package/templates/svelte/src/lib/render.ts +15 -0
  65. package/templates/svelte/src/lib/services/api-client.ts +28 -0
  66. package/templates/svelte/src/main.ts +27 -0
  67. package/templates/svelte/src/pages/About.svelte +10 -0
  68. package/templates/svelte/src/pages/Home.svelte +34 -0
  69. package/templates/svelte/src/pages/NotFound.svelte +23 -0
  70. package/templates/svelte/src/pages/ProductDetail.svelte +32 -0
  71. package/templates/svelte/src/pages/Search.svelte +29 -0
  72. package/templates/svelte/src/ssr.ts +12 -0
  73. package/templates/svelte/tsconfig.json +16 -0
  74. package/templates/svelte/vite.config.ts +21 -0
  75. package/templates/svelte-minimal/env.d.ts +9 -0
  76. package/templates/svelte-minimal/index.html +13 -0
  77. package/templates/svelte-minimal/package.json +18 -0
  78. package/templates/svelte-minimal/src/App.svelte +63 -0
  79. package/templates/svelte-minimal/src/bootstrap.ts +6 -0
  80. package/templates/svelte-minimal/src/lib/controllers/home.ts +15 -0
  81. package/templates/svelte-minimal/src/lib/framework-svelte.ts +34 -0
  82. package/templates/svelte-minimal/src/lib/render.ts +12 -0
  83. package/templates/svelte-minimal/src/main.ts +22 -0
  84. package/templates/svelte-minimal/src/pages/Home.svelte +10 -0
  85. package/templates/svelte-minimal/src/ssr.ts +18 -0
  86. package/templates/svelte-minimal/tsconfig.json +16 -0
  87. package/templates/svelte-minimal/vite.config.ts +12 -0
  88. package/templates/vue/env.d.ts +7 -0
  89. package/templates/vue/index.html +13 -0
  90. package/templates/vue/package.json +18 -0
  91. package/templates/vue/src/App.vue +29 -0
  92. package/templates/vue/src/actions.ts +14 -0
  93. package/templates/vue/src/bootstrap.ts +42 -0
  94. package/templates/vue/src/components/Layout.vue +18 -0
  95. package/templates/vue/src/components/Loading.vue +3 -0
  96. package/templates/vue/src/components/Navigation.vue +58 -0
  97. package/templates/vue/src/components/PageRenderer.vue +26 -0
  98. package/templates/vue/src/components/ProductCard.vue +39 -0
  99. package/templates/vue/src/lib/controllers/about.ts +18 -0
  100. package/templates/vue/src/lib/controllers/error.ts +11 -0
  101. package/templates/vue/src/lib/controllers/home.ts +53 -0
  102. package/templates/vue/src/lib/controllers/product-detail.ts +52 -0
  103. package/templates/vue/src/lib/controllers/search.ts +58 -0
  104. package/templates/vue/src/lib/guards/auth.ts +17 -0
  105. package/templates/vue/src/lib/guards/seo.ts +15 -0
  106. package/templates/vue/src/lib/mappers/product.ts +27 -0
  107. package/templates/vue/src/lib/models/product.ts +47 -0
  108. package/templates/vue/src/lib/services/api-client.ts +31 -0
  109. package/templates/vue/src/main.ts +46 -0
  110. package/templates/vue/src/pages/About.vue +16 -0
  111. package/templates/vue/src/pages/Home.vue +38 -0
  112. package/templates/vue/src/pages/NotFound.vue +22 -0
  113. package/templates/vue/src/pages/ProductDetail.vue +34 -0
  114. package/templates/vue/src/pages/Search.vue +33 -0
  115. package/templates/vue/src/ssr.ts +24 -0
  116. package/templates/vue/tsconfig.json +17 -0
  117. package/templates/vue/vite.config.ts +20 -0
  118. package/templates/vue-minimal/env.d.ts +7 -0
  119. package/templates/vue-minimal/index.html +13 -0
  120. package/templates/vue-minimal/package.json +18 -0
  121. package/templates/vue-minimal/src/App.vue +21 -0
  122. package/templates/vue-minimal/src/bootstrap.ts +6 -0
  123. package/templates/vue-minimal/src/lib/controllers/home.ts +15 -0
  124. package/templates/vue-minimal/src/main.ts +41 -0
  125. package/templates/vue-minimal/src/pages/Home.vue +12 -0
  126. package/templates/vue-minimal/src/ssr.ts +28 -0
  127. package/templates/vue-minimal/tsconfig.json +17 -0
  128. package/templates/vue-minimal/vite.config.ts +12 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,129 @@
1
+ import * as prompts from "@clack/prompts";
2
+ import { bold, cyan, green, red } from "kolorist";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ //#region src/index.ts
6
+ const FRAMEWORKS = [
7
+ {
8
+ name: "vue",
9
+ display: "Vue",
10
+ color: green,
11
+ variants: [{
12
+ name: "vue",
13
+ display: "Full (SSR + routing + DI + guards)"
14
+ }, {
15
+ name: "vue-minimal",
16
+ display: "Minimal"
17
+ }]
18
+ },
19
+ {
20
+ name: "react",
21
+ display: "React",
22
+ color: cyan,
23
+ variants: [{
24
+ name: "react",
25
+ display: "Full (SSR + routing + DI + guards)"
26
+ }, {
27
+ name: "react-minimal",
28
+ display: "Minimal"
29
+ }]
30
+ },
31
+ {
32
+ name: "svelte",
33
+ display: "Svelte",
34
+ color: red,
35
+ variants: [{
36
+ name: "svelte",
37
+ display: "Full (SSR + routing + DI + guards)"
38
+ }, {
39
+ name: "svelte-minimal",
40
+ display: "Minimal"
41
+ }]
42
+ }
43
+ ];
44
+ async function main() {
45
+ prompts.intro(bold("Create Finesoft App"));
46
+ const project = await prompts.group({
47
+ name: () => prompts.text({
48
+ message: "Project name",
49
+ placeholder: "my-finesoft-app",
50
+ defaultValue: "my-finesoft-app",
51
+ validate: (value) => {
52
+ if (!value) return "Project name is required";
53
+ if (/[^\w\-.]/.test(value)) return "Invalid project name";
54
+ }
55
+ }),
56
+ framework: () => prompts.select({
57
+ message: "Framework",
58
+ options: FRAMEWORKS.map((f) => ({
59
+ value: f.name,
60
+ label: f.color(f.display)
61
+ }))
62
+ }),
63
+ variant: ({ results }) => {
64
+ const fw = FRAMEWORKS.find((f) => f.name === results.framework);
65
+ return prompts.select({
66
+ message: "Template",
67
+ options: fw.variants.map((v) => ({
68
+ value: v.name,
69
+ label: v.display
70
+ }))
71
+ });
72
+ }
73
+ }, { onCancel: () => {
74
+ prompts.cancel("Operation cancelled.");
75
+ process.exit(0);
76
+ } });
77
+ const targetDir = path.resolve(process.cwd(), project.name);
78
+ const templateName = project.variant;
79
+ const templateDir = path.resolve(new URL(".", import.meta.url).pathname, "..", "templates", templateName);
80
+ if (!fs.existsSync(templateDir)) {
81
+ prompts.log.error(`Template "${templateName}" not found at ${templateDir}`);
82
+ process.exit(1);
83
+ }
84
+ if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
85
+ const overwrite = await prompts.confirm({ message: `Directory "${project.name}" is not empty. Overwrite?` });
86
+ if (!overwrite || prompts.isCancel(overwrite)) {
87
+ prompts.cancel("Operation cancelled.");
88
+ process.exit(0);
89
+ }
90
+ fs.rmSync(targetDir, {
91
+ recursive: true,
92
+ force: true
93
+ });
94
+ }
95
+ copyDir(templateDir, targetDir);
96
+ const pkgPath = path.join(targetDir, "package.json");
97
+ if (fs.existsSync(pkgPath)) {
98
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
99
+ pkg.name = project.name;
100
+ if (pkg.devDependencies) {
101
+ for (const [key, value] of Object.entries(pkg.devDependencies)) if (value === "catalog:") pkg.devDependencies[key] = "latest";
102
+ }
103
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 4) + "\n");
104
+ }
105
+ const fw = FRAMEWORKS.find((f) => f.name === project.framework);
106
+ prompts.log.success(green(`Created ${bold(project.name)} with ${fw.color(fw.display)}`));
107
+ prompts.note([
108
+ `cd ${project.name}`,
109
+ "vp install",
110
+ "vp dev"
111
+ ].join("\n"), "Next steps");
112
+ prompts.outro("Happy coding!");
113
+ }
114
+ function copyDir(src, dest) {
115
+ fs.mkdirSync(dest, { recursive: true });
116
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
117
+ const srcPath = path.join(src, entry.name);
118
+ const destPath = path.join(dest, entry.name);
119
+ if (entry.name === "node_modules" || entry.name === "dist") continue;
120
+ if (entry.isDirectory()) copyDir(srcPath, destPath);
121
+ else fs.copyFileSync(srcPath, destPath);
122
+ }
123
+ }
124
+ main().catch((err) => {
125
+ console.error(err);
126
+ process.exit(1);
127
+ });
128
+ //#endregion
129
+ export {};
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@finesoft/create-app",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a new Finesoft Front project",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "create-finesoft-app": "dist/index.mjs"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "templates"
12
+ ],
13
+ "type": "module",
14
+ "dependencies": {
15
+ "@clack/prompts": "^0.10.0",
16
+ "kolorist": "^1.8.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^22",
20
+ "vite-plus": "latest"
21
+ },
22
+ "scripts": {
23
+ "build": "vp pack",
24
+ "dev": "vp pack --watch"
25
+ }
26
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="<!--ssr-lang-->">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <!--ssr-head-->
7
+ </head>
8
+ <body>
9
+ <div id="app"><!--ssr-body--></div>
10
+ <!--ssr-data-->
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "finesoft-react",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vp dev",
7
+ "build": "vp build",
8
+ "preview": "vp preview"
9
+ },
10
+ "dependencies": {
11
+ "@finesoft/front": "workspace:*",
12
+ "react": "^19.0.0",
13
+ "react-dom": "^19.0.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/react": "^19.0.0",
17
+ "@types/react-dom": "^19.0.0",
18
+ "@vitejs/plugin-react": "^4.0.0",
19
+ "vite-plus": "catalog:"
20
+ }
21
+ }
@@ -0,0 +1,20 @@
1
+ import type { Action } from "@finesoft/front";
2
+ import { Layout } from "./components/Layout";
3
+ import { Loading } from "./components/Loading";
4
+ import { PageRenderer } from "./components/PageRenderer";
5
+ import type { AppPage } from "./lib/models/product";
6
+
7
+ interface AppProps {
8
+ page?: AppPage | null;
9
+ loading?: boolean;
10
+ onAction?: (action: Action) => void;
11
+ }
12
+
13
+ export default function App({ page, loading = false, onAction }: AppProps) {
14
+ return (
15
+ <Layout currentPath={page?.url ?? "/"} onAction={onAction}>
16
+ {loading && <Loading />}
17
+ {page && <PageRenderer page={page} onAction={onAction} />}
18
+ </Layout>
19
+ );
20
+ }
@@ -0,0 +1,14 @@
1
+ import { makeExternalUrlAction, makeFlowAction, type Action } from "@finesoft/front";
2
+
3
+ /** 创建指向商品详情的 FlowAction */
4
+ export function productDetailAction(id: string): Action {
5
+ return makeFlowAction(`/products/${id}`);
6
+ }
7
+
8
+ /** 导航 Actions */
9
+ export const NAV_ACTIONS = {
10
+ home: makeFlowAction("/"),
11
+ search: makeFlowAction("/search"),
12
+ about: makeFlowAction("/about"),
13
+ github: makeExternalUrlAction("https://github.com/nicepkg/finesoft"),
14
+ } as const;
@@ -0,0 +1,36 @@
1
+ import { type Framework, defineRoutes } from "@finesoft/front";
2
+ import { AboutController } from "./lib/controllers/about";
3
+ import { HomeController } from "./lib/controllers/home";
4
+ import { ProductDetailController } from "./lib/controllers/product-detail";
5
+ import { SearchController } from "./lib/controllers/search";
6
+ import { authGuard } from "./lib/guards/auth";
7
+ import { seoGuard } from "./lib/guards/seo";
8
+
9
+ export function bootstrap(framework: Framework): void {
10
+ framework.afterLoad(seoGuard);
11
+
12
+ defineRoutes(framework, [
13
+ { path: "/", intentId: "home", controller: new HomeController() },
14
+ {
15
+ path: "/products/:id",
16
+ intentId: "product-detail",
17
+ controller: new ProductDetailController(),
18
+ },
19
+ {
20
+ path: "/search",
21
+ intentId: "search",
22
+ controller: new SearchController(),
23
+ },
24
+ {
25
+ path: "/about",
26
+ intentId: "about",
27
+ controller: new AboutController(),
28
+ renderMode: "csr",
29
+ },
30
+ {
31
+ path: "/admin",
32
+ intentId: "home",
33
+ beforeLoad: [authGuard],
34
+ },
35
+ ]);
36
+ }
@@ -0,0 +1,63 @@
1
+ import type { Action } from "@finesoft/front";
2
+ import { NAV_ACTIONS } from "../actions";
3
+
4
+ interface LayoutProps {
5
+ children: React.ReactNode;
6
+ currentPath?: string;
7
+ onAction?: (action: Action) => void;
8
+ }
9
+
10
+ /** 公共布局 — 顶部导航栏 + 内容区 */
11
+ export function Layout({ children, currentPath = "/", onAction }: LayoutProps) {
12
+ const handleNav = (action: Action) => (e: React.MouseEvent) => {
13
+ e.preventDefault();
14
+ onAction?.(action);
15
+ };
16
+
17
+ const navItems = [
18
+ { label: "Home", action: NAV_ACTIONS.home, path: "/" },
19
+ { label: "Search", action: NAV_ACTIONS.search, path: "/search" },
20
+ { label: "About", action: NAV_ACTIONS.about, path: "/about" },
21
+ ];
22
+
23
+ return (
24
+ <div>
25
+ <nav
26
+ style={{
27
+ display: "flex",
28
+ gap: "1rem",
29
+ padding: "1rem",
30
+ borderBottom: "1px solid #eee",
31
+ alignItems: "center",
32
+ }}
33
+ >
34
+ {navItems.map((item) => (
35
+ <a
36
+ key={item.path}
37
+ href={item.path}
38
+ style={{
39
+ textDecoration: "none",
40
+ color: currentPath === item.path ? "#007bff" : "#333",
41
+ fontWeight: currentPath === item.path ? "bold" : "normal",
42
+ }}
43
+ onClick={handleNav(item.action)}
44
+ >
45
+ {item.label}
46
+ </a>
47
+ ))}
48
+ <a
49
+ href={NAV_ACTIONS.github.url}
50
+ style={{
51
+ marginLeft: "auto",
52
+ textDecoration: "none",
53
+ color: "#333",
54
+ }}
55
+ onClick={handleNav(NAV_ACTIONS.github)}
56
+ >
57
+ GitHub ↗
58
+ </a>
59
+ </nav>
60
+ <main style={{ padding: "1rem" }}>{children}</main>
61
+ </div>
62
+ );
63
+ }
@@ -0,0 +1,3 @@
1
+ export function Loading() {
2
+ return <div style={{ padding: "2rem", textAlign: "center", color: "#999" }}>Loading…</div>;
3
+ }
@@ -0,0 +1,51 @@
1
+ import type { Action } from "@finesoft/front";
2
+ import { makeExternalUrlAction, makeFlowAction } from "@finesoft/front";
3
+
4
+ const links = [
5
+ { label: "Home", action: makeFlowAction("/"), path: "/" },
6
+ { label: "Search", action: makeFlowAction("/search"), path: "/search" },
7
+ { label: "About", action: makeFlowAction("/about"), path: "/about" },
8
+ {
9
+ label: "GitHub",
10
+ action: makeExternalUrlAction("https://github.com/nicepkg/finesoft"),
11
+ path: null,
12
+ },
13
+ ];
14
+
15
+ interface NavigationProps {
16
+ currentPath?: string;
17
+ onAction?: (action: Action) => void;
18
+ }
19
+
20
+ export default function Navigation({ currentPath = "/", onAction }: NavigationProps) {
21
+ const handleNav = (action: Action) => (e: React.MouseEvent) => {
22
+ e.preventDefault();
23
+ onAction?.(action);
24
+ };
25
+
26
+ return (
27
+ <nav
28
+ style={{
29
+ display: "flex",
30
+ gap: "1rem",
31
+ padding: "1rem",
32
+ borderBottom: "1px solid #eee",
33
+ }}
34
+ >
35
+ {links.map((link) => (
36
+ <a
37
+ key={link.label}
38
+ href={link.action.url}
39
+ style={{
40
+ textDecoration: "none",
41
+ color: link.path === currentPath ? "#007bff" : "#333",
42
+ fontWeight: link.path === currentPath ? "bold" : "normal",
43
+ }}
44
+ onClick={handleNav(link.action)}
45
+ >
46
+ {link.label}
47
+ </a>
48
+ ))}
49
+ </nav>
50
+ );
51
+ }
@@ -0,0 +1,41 @@
1
+ import type { Action } from "@finesoft/front";
2
+ import type { AppPage } from "../lib/models/product";
3
+ import About from "../pages/About";
4
+ import Home from "../pages/Home";
5
+ import NotFound from "../pages/NotFound";
6
+ import ProductDetail from "../pages/ProductDetail";
7
+ import Search from "../pages/Search";
8
+
9
+ interface PageRendererProps {
10
+ page: AppPage;
11
+ onAction?: (action: Action) => void;
12
+ }
13
+
14
+ /** 根据 pageType 分发渲染对应组件 */
15
+ export function PageRenderer({ page, onAction }: PageRendererProps) {
16
+ switch (page.pageType) {
17
+ case "home":
18
+ return <Home page={page} onAction={onAction} />;
19
+ case "product":
20
+ return <ProductDetail page={page} onAction={onAction} />;
21
+ case "search":
22
+ return <Search page={page} onAction={onAction} />;
23
+ case "about":
24
+ return <About page={page} onAction={onAction} />;
25
+ case "error":
26
+ return <NotFound page={page} onAction={onAction} />;
27
+ default:
28
+ return (
29
+ <NotFound
30
+ page={{
31
+ id: "unknown",
32
+ pageType: "error",
33
+ title: "Unknown Page",
34
+ description: "Page not found",
35
+ status: 404,
36
+ }}
37
+ onAction={onAction}
38
+ />
39
+ );
40
+ }
41
+ }
@@ -0,0 +1,35 @@
1
+ import type { Action } from "@finesoft/front";
2
+ import type { ProductItem } from "../lib/models/product";
3
+
4
+ interface ProductCardProps {
5
+ item: ProductItem;
6
+ onAction?: (action: Action) => void;
7
+ }
8
+
9
+ export default function ProductCard({ item, onAction }: ProductCardProps) {
10
+ const handleClick = item.clickAction
11
+ ? (e: React.MouseEvent) => {
12
+ e.preventDefault();
13
+ onAction?.(item.clickAction!);
14
+ }
15
+ : undefined;
16
+
17
+ return (
18
+ <div
19
+ style={{
20
+ border: "1px solid #eee",
21
+ borderRadius: "8px",
22
+ padding: "1rem",
23
+ minWidth: "200px",
24
+ }}
25
+ >
26
+ <h3>{item.name}</h3>
27
+ <p style={{ color: "#007bff", fontWeight: "bold" }}>${item.price.toFixed(2)}</p>
28
+ {item.clickAction && "url" in item.clickAction && (
29
+ <a href={item.clickAction.url} onClick={handleClick}>
30
+ View Details &rarr;
31
+ </a>
32
+ )}
33
+ </div>
34
+ );
35
+ }
@@ -0,0 +1,18 @@
1
+ import { BaseController } from "@finesoft/front";
2
+ import type { AboutPage } from "../models/product";
3
+
4
+ export class AboutController extends BaseController<Record<string, string>, AboutPage> {
5
+ readonly intentId = "about";
6
+
7
+ execute(): AboutPage {
8
+ return {
9
+ id: "about",
10
+ pageType: "about",
11
+ title: "About",
12
+ description: "About Finesoft Front",
13
+ url: "/about",
14
+ content:
15
+ "Finesoft Front is a full-stack TypeScript framework with router, DI, middleware, SSR, and multi-platform deployment.",
16
+ };
17
+ }
18
+ }
@@ -0,0 +1,11 @@
1
+ import type { ErrorPage } from "../models/product";
2
+
3
+ export function getErrorPage(status: number, message: string): ErrorPage {
4
+ return {
5
+ id: "error",
6
+ pageType: "error",
7
+ title: `Error ${status}`,
8
+ description: message,
9
+ status,
10
+ };
11
+ }
@@ -0,0 +1,53 @@
1
+ import { BaseController, makeFlowAction } from "@finesoft/front";
2
+ import type { HomePage, ProductItem, ProductShelf } from "../models/product";
3
+
4
+ const MOCK_PRODUCTS: ProductItem[] = [
5
+ {
6
+ id: "1",
7
+ itemType: "product",
8
+ name: "TypeScript Handbook",
9
+ price: 29.99,
10
+ imageUrl: "/img/ts.svg",
11
+ clickAction: makeFlowAction("/products/1"),
12
+ },
13
+ {
14
+ id: "2",
15
+ itemType: "product",
16
+ name: "Vite Starter Kit",
17
+ price: 19.99,
18
+ imageUrl: "/img/vite.svg",
19
+ clickAction: makeFlowAction("/products/2"),
20
+ },
21
+ {
22
+ id: "3",
23
+ itemType: "product",
24
+ name: "Hono Framework Guide",
25
+ price: 24.99,
26
+ imageUrl: "/img/hono.svg",
27
+ clickAction: makeFlowAction("/products/3"),
28
+ },
29
+ ];
30
+
31
+ export class HomeController extends BaseController<Record<string, string>, HomePage> {
32
+ readonly intentId = "home";
33
+
34
+ execute(): HomePage {
35
+ const shelf: ProductShelf = {
36
+ id: "featured",
37
+ shelfType: "products",
38
+ title: "Featured Products",
39
+ isHorizontal: true,
40
+ seeAllAction: makeFlowAction("/search"),
41
+ items: MOCK_PRODUCTS,
42
+ };
43
+
44
+ return {
45
+ id: "home",
46
+ pageType: "home",
47
+ title: "Home",
48
+ description: "Welcome to Finesoft Front Demo",
49
+ url: "/",
50
+ shelves: [shelf],
51
+ };
52
+ }
53
+ }
@@ -0,0 +1,52 @@
1
+ import {
2
+ BaseController,
3
+ type Container,
4
+ DEP_KEYS,
5
+ type Logger,
6
+ type LoggerFactory,
7
+ } from "@finesoft/front";
8
+ import type { ProductPage } from "../models/product";
9
+
10
+ export class ProductDetailController extends BaseController<{ id: string }, ProductPage> {
11
+ readonly intentId = "product-detail";
12
+
13
+ async execute(params: { id: string }, container: Container): Promise<ProductPage> {
14
+ const loggerFactory = container.resolve<LoggerFactory>(DEP_KEYS.LOGGER_FACTORY);
15
+ const log: Logger = loggerFactory.loggerFor("ProductDetailController");
16
+ log.info(`Loading product ${params.id}`);
17
+
18
+ // Simulated API fetch — replace with real HttpClient call
19
+ return {
20
+ id: `product-${params.id}`,
21
+ pageType: "product",
22
+ title: `Product ${params.id}`,
23
+ description: `Details for product ${params.id}`,
24
+ url: `/products/${params.id}`,
25
+ product: {
26
+ id: params.id,
27
+ name: `Product ${params.id}`,
28
+ price: 29.99,
29
+ description:
30
+ "This is a demo product showcasing BaseController with typed params and DI container usage.",
31
+ imageUrl: `/img/product-${params.id}.svg`,
32
+ },
33
+ };
34
+ }
35
+
36
+ fallback(params: { id: string }, error: Error): ProductPage {
37
+ return {
38
+ id: `product-${params.id}`,
39
+ pageType: "product",
40
+ title: "Product Not Found",
41
+ description: error.message,
42
+ url: `/products/${params.id}`,
43
+ product: {
44
+ id: params.id,
45
+ name: "Unknown Product",
46
+ price: 0,
47
+ description: "Could not load product data.",
48
+ imageUrl: "/img/placeholder.svg",
49
+ },
50
+ };
51
+ }
52
+ }
@@ -0,0 +1,58 @@
1
+ import { BaseController, makeFlowAction } from "@finesoft/front";
2
+ import type { ProductItem, SearchPage } from "../models/product";
3
+
4
+ const ALL_PRODUCTS: ProductItem[] = [
5
+ {
6
+ id: "1",
7
+ itemType: "product",
8
+ name: "TypeScript Handbook",
9
+ price: 29.99,
10
+ imageUrl: "/img/ts.svg",
11
+ clickAction: makeFlowAction("/products/1"),
12
+ },
13
+ {
14
+ id: "2",
15
+ itemType: "product",
16
+ name: "Vite Starter Kit",
17
+ price: 19.99,
18
+ imageUrl: "/img/vite.svg",
19
+ clickAction: makeFlowAction("/products/2"),
20
+ },
21
+ {
22
+ id: "3",
23
+ itemType: "product",
24
+ name: "Hono Framework Guide",
25
+ price: 24.99,
26
+ imageUrl: "/img/hono.svg",
27
+ clickAction: makeFlowAction("/products/3"),
28
+ },
29
+ {
30
+ id: "4",
31
+ itemType: "product",
32
+ name: "SSR Deep Dive",
33
+ price: 34.99,
34
+ imageUrl: "/img/ssr.svg",
35
+ clickAction: makeFlowAction("/products/4"),
36
+ },
37
+ ];
38
+
39
+ export class SearchController extends BaseController<{ q?: string }, SearchPage> {
40
+ readonly intentId = "search";
41
+
42
+ execute(params: { q?: string }): SearchPage {
43
+ const query = params.q ?? "";
44
+ const results = query
45
+ ? ALL_PRODUCTS.filter((p) => p.name.toLowerCase().includes(query.toLowerCase()))
46
+ : ALL_PRODUCTS;
47
+
48
+ return {
49
+ id: "search",
50
+ pageType: "search",
51
+ title: query ? `Search: ${query}` : "All Products",
52
+ description: `${results.length} result(s)`,
53
+ url: "/search",
54
+ query,
55
+ results,
56
+ };
57
+ }
58
+ }