@finesoft/create-app 0.1.33 → 0.1.35
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/dist/index.mjs +22 -26
- package/package.json +14 -5
- package/templates/react/README.md +35 -0
- package/templates/react/index.html +2 -3
- package/templates/react/package.json +12 -8
- package/templates/react/src/App.tsx +7 -15
- package/templates/react/src/app-definition.ts +35 -0
- package/templates/react/src/components/Layout.tsx +5 -52
- package/templates/react/src/components/Loading.tsx +1 -1
- package/templates/react/src/components/Navigation.tsx +10 -37
- package/templates/react/src/components/ProductCard.tsx +5 -23
- package/templates/react/src/config.ts +2 -0
- package/templates/react/src/lib/controllers/about.ts +18 -13
- package/templates/react/src/lib/controllers/error.ts +11 -7
- package/templates/react/src/lib/controllers/home.ts +36 -41
- package/templates/react/src/lib/controllers/product-detail.ts +71 -40
- package/templates/react/src/lib/controllers/search.ts +33 -54
- package/templates/react/src/lib/data/products.ts +36 -0
- package/templates/react/src/lib/mappers/product.ts +3 -3
- package/templates/react/src/lib/models/product.ts +11 -1
- package/templates/react/src/lib/services/api-client.ts +7 -4
- package/templates/react/src/main.ts +34 -0
- package/templates/react/src/pages/About.tsx +2 -4
- package/templates/react/src/pages/Home.tsx +6 -15
- package/templates/react/src/pages/NotFound.tsx +4 -14
- package/templates/react/src/pages/ProductDetail.tsx +5 -23
- package/templates/react/src/pages/Search.tsx +5 -7
- package/templates/react/src/ssr.ts +11 -0
- package/templates/react/src/styles.css +71 -0
- package/templates/react/src/views.ts +12 -0
- package/templates/react/tsconfig.json +14 -9
- package/templates/react/vite.config.ts +9 -13
- package/templates/react-minimal/README.md +39 -0
- package/templates/react-minimal/index.html +2 -3
- package/templates/react-minimal/package.json +12 -8
- package/templates/react-minimal/src/App.tsx +40 -85
- package/templates/react-minimal/src/app-definition.ts +48 -0
- package/templates/react-minimal/src/config.ts +2 -0
- package/templates/react-minimal/src/lib/controllers/detail.ts +19 -18
- package/templates/react-minimal/src/lib/controllers/error.ts +15 -0
- package/templates/react-minimal/src/lib/controllers/home.ts +19 -24
- package/templates/react-minimal/src/lib/controllers/notes.ts +7 -5
- package/templates/react-minimal/src/lib/locale.ts +16 -0
- package/templates/react-minimal/src/lib/models/page.ts +27 -0
- package/templates/react-minimal/src/lib/navigation.ts +11 -0
- package/templates/react-minimal/src/locales/en-US.json +5 -0
- package/templates/react-minimal/src/locales/zh-Hans.json +5 -0
- package/templates/react-minimal/src/main.ts +43 -0
- package/templates/react-minimal/src/pages/Detail.tsx +20 -0
- package/templates/react-minimal/src/pages/Home.tsx +38 -0
- package/templates/react-minimal/src/pages/NotFound.tsx +11 -0
- package/templates/react-minimal/src/pages/Notes.tsx +21 -0
- package/templates/react-minimal/src/ssr.ts +11 -0
- package/templates/react-minimal/src/styles.css +74 -0
- package/templates/react-minimal/src/views.ts +6 -0
- package/templates/react-minimal/src/vite-env.d.ts +3 -0
- package/templates/react-minimal/tsconfig.json +14 -9
- package/templates/react-minimal/vite.config.ts +9 -7
- package/templates/svelte/README.md +35 -0
- package/templates/svelte/index.html +1 -2
- package/templates/svelte/package.json +9 -5
- package/templates/svelte/src/App.svelte +8 -80
- package/templates/svelte/src/app-definition.ts +35 -0
- package/templates/svelte/src/components/Layout.svelte +2 -2
- package/templates/svelte/src/components/Loading.svelte +1 -1
- package/templates/svelte/src/components/Navigation.svelte +5 -66
- package/templates/svelte/src/components/ProductCard.svelte +3 -24
- package/templates/svelte/src/config.ts +2 -0
- package/templates/svelte/src/lib/controllers/about.ts +18 -13
- package/templates/svelte/src/lib/controllers/error.ts +11 -7
- package/templates/svelte/src/lib/controllers/home.ts +36 -41
- package/templates/svelte/src/lib/controllers/product-detail.ts +71 -40
- package/templates/svelte/src/lib/controllers/search.ts +33 -54
- package/templates/svelte/src/lib/data/products.ts +36 -0
- package/templates/svelte/src/lib/mappers/product.ts +3 -3
- package/templates/svelte/src/lib/models/product.ts +12 -1
- package/templates/svelte/src/lib/services/api-client.ts +7 -4
- package/templates/svelte/src/main.ts +28 -24
- package/templates/svelte/src/pages/About.svelte +2 -2
- package/templates/svelte/src/pages/Home.svelte +3 -15
- package/templates/svelte/src/pages/NotFound.svelte +3 -12
- package/templates/svelte/src/pages/ProductDetail.svelte +4 -21
- package/templates/svelte/src/pages/Search.svelte +2 -10
- package/templates/svelte/src/ssr.ts +10 -9
- package/templates/svelte/src/styles.css +71 -0
- package/templates/svelte/src/views.ts +12 -0
- package/templates/svelte/svelte.config.js +2 -0
- package/templates/svelte/tsconfig.json +13 -8
- package/templates/svelte/vite.config.ts +9 -13
- package/templates/svelte-minimal/README.md +39 -0
- package/templates/svelte-minimal/index.html +1 -2
- package/templates/svelte-minimal/package.json +9 -5
- package/templates/svelte-minimal/src/App.svelte +49 -61
- package/templates/svelte-minimal/src/app-definition.ts +48 -0
- package/templates/svelte-minimal/src/config.ts +2 -0
- package/templates/svelte-minimal/src/lib/controllers/detail.ts +23 -0
- package/templates/svelte-minimal/src/lib/controllers/error.ts +15 -0
- package/templates/svelte-minimal/src/lib/controllers/home.ts +24 -23
- package/templates/svelte-minimal/src/lib/controllers/notes.ts +18 -0
- package/templates/svelte-minimal/src/lib/locale.ts +16 -0
- package/templates/svelte-minimal/src/lib/models/page.ts +27 -0
- package/templates/svelte-minimal/src/lib/navigation.ts +11 -0
- package/templates/svelte-minimal/src/locales/en-US.json +1 -3
- package/templates/svelte-minimal/src/locales/zh-Hans.json +1 -3
- package/templates/svelte-minimal/src/main.ts +37 -19
- package/templates/svelte-minimal/src/pages/Detail.svelte +11 -0
- package/templates/svelte-minimal/src/pages/Home.svelte +17 -21
- package/templates/svelte-minimal/src/pages/NotFound.svelte +10 -0
- package/templates/svelte-minimal/src/pages/Notes.svelte +11 -0
- package/templates/svelte-minimal/src/ssr.ts +9 -17
- package/templates/svelte-minimal/src/styles.css +74 -0
- package/templates/svelte-minimal/src/views.ts +6 -0
- package/templates/svelte-minimal/src/vite-env.d.ts +3 -0
- package/templates/svelte-minimal/svelte.config.js +2 -0
- package/templates/svelte-minimal/tsconfig.json +13 -8
- package/templates/svelte-minimal/vite.config.ts +9 -10
- package/templates/vue/README.md +35 -0
- package/templates/vue/index.html +1 -2
- package/templates/vue/package.json +9 -5
- package/templates/vue/src/App.vue +7 -22
- package/templates/vue/src/app-definition.ts +35 -0
- package/templates/vue/src/components/Layout.vue +4 -6
- package/templates/vue/src/components/Loading.vue +1 -1
- package/templates/vue/src/components/Navigation.vue +10 -43
- package/templates/vue/src/components/ProductCard.vue +4 -28
- package/templates/vue/src/config.ts +2 -0
- package/templates/vue/src/lib/controllers/about.ts +18 -13
- package/templates/vue/src/lib/controllers/error.ts +11 -7
- package/templates/vue/src/lib/controllers/home.ts +36 -41
- package/templates/vue/src/lib/controllers/product-detail.ts +71 -40
- package/templates/vue/src/lib/controllers/search.ts +33 -54
- package/templates/vue/src/lib/data/products.ts +36 -0
- package/templates/vue/src/lib/mappers/product.ts +3 -3
- package/templates/vue/src/lib/models/product.ts +12 -1
- package/templates/vue/src/lib/services/api-client.ts +7 -7
- package/templates/vue/src/main.ts +28 -43
- package/templates/vue/src/pages/About.vue +3 -5
- package/templates/vue/src/pages/Home.vue +5 -23
- package/templates/vue/src/pages/NotFound.vue +4 -13
- package/templates/vue/src/pages/ProductDetail.vue +5 -23
- package/templates/vue/src/pages/Search.vue +4 -18
- package/templates/vue/src/ssr.ts +5 -18
- package/templates/vue/src/styles.css +71 -0
- package/templates/vue/src/views.ts +12 -0
- package/templates/vue/tsconfig.json +14 -9
- package/templates/vue/vite.config.ts +9 -13
- package/templates/vue-minimal/README.md +39 -0
- package/templates/vue-minimal/index.html +1 -2
- package/templates/vue-minimal/package.json +9 -5
- package/templates/vue-minimal/src/App.vue +46 -43
- package/templates/vue-minimal/src/app-definition.ts +48 -0
- package/templates/vue-minimal/src/config.ts +2 -0
- package/templates/vue-minimal/src/lib/controllers/detail.ts +19 -18
- package/templates/vue-minimal/src/lib/controllers/error.ts +15 -0
- package/templates/vue-minimal/src/lib/controllers/home.ts +19 -24
- package/templates/vue-minimal/src/lib/controllers/notes.ts +7 -5
- package/templates/vue-minimal/src/lib/locale.ts +16 -0
- package/templates/vue-minimal/src/lib/models/page.ts +27 -0
- package/templates/vue-minimal/src/lib/navigation.ts +11 -0
- package/templates/vue-minimal/src/locales/en-US.json +5 -0
- package/templates/vue-minimal/src/locales/zh-Hans.json +5 -0
- package/templates/vue-minimal/src/main.ts +35 -70
- package/templates/vue-minimal/src/pages/Detail.vue +20 -0
- package/templates/vue-minimal/src/pages/Home.vue +34 -0
- package/templates/vue-minimal/src/pages/NotFound.vue +13 -0
- package/templates/vue-minimal/src/pages/Notes.vue +22 -0
- package/templates/vue-minimal/src/ssr.ts +7 -46
- package/templates/vue-minimal/src/styles.css +74 -0
- package/templates/vue-minimal/src/views.ts +6 -0
- package/templates/vue-minimal/src/vite-env.d.ts +3 -0
- package/templates/vue-minimal/tsconfig.json +14 -9
- package/templates/vue-minimal/vite.config.ts +9 -7
- package/templates/react/src/actions.ts +0 -14
- package/templates/react/src/bootstrap.ts +0 -36
- package/templates/react/src/components/PageRenderer.tsx +0 -41
- package/templates/react/src/main.tsx +0 -49
- package/templates/react/src/ssr.tsx +0 -23
- package/templates/react-minimal/src/bootstrap.ts +0 -39
- package/templates/react-minimal/src/main.tsx +0 -99
- package/templates/react-minimal/src/ssr.tsx +0 -51
- package/templates/react-minimal/src/views/DetailView.tsx +0 -28
- package/templates/react-minimal/src/views/HomeView.tsx +0 -27
- package/templates/react-minimal/src/views/NotesView.tsx +0 -11
- package/templates/svelte/src/actions.ts +0 -14
- package/templates/svelte/src/bootstrap.ts +0 -36
- package/templates/svelte/src/components/PageRenderer.svelte +0 -22
- package/templates/svelte/src/lib/framework-svelte.ts +0 -34
- package/templates/svelte/src/lib/render.ts +0 -15
- package/templates/svelte-minimal/src/bootstrap.ts +0 -6
- package/templates/svelte-minimal/src/lib/framework-svelte.ts +0 -34
- package/templates/svelte-minimal/src/lib/render.ts +0 -12
- package/templates/vue/src/actions.ts +0 -14
- package/templates/vue/src/bootstrap.ts +0 -42
- package/templates/vue/src/components/PageRenderer.vue +0 -26
- package/templates/vue-minimal/src/bootstrap.ts +0 -39
- package/templates/vue-minimal/src/views/DetailView.vue +0 -29
- package/templates/vue-minimal/src/views/HomeView.vue +0 -26
- package/templates/vue-minimal/src/views/NotesView.vue +0 -12
|
@@ -1,52 +1,83 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
DEP_KEYS,
|
|
5
|
-
type Logger,
|
|
6
|
-
type LoggerFactory,
|
|
7
|
-
} from "@finesoft/front";
|
|
1
|
+
import { DEP_KEYS, type Logger, type LoggerFactory } from "@finesoft/front";
|
|
2
|
+
import { BaseServerController } from "@finesoft/front";
|
|
3
|
+
import { markPublic } from "@finesoft/front";
|
|
8
4
|
import type { ProductPage } from "../models/product";
|
|
5
|
+
import type {
|
|
6
|
+
ProductDetailControllerFailure as Failure,
|
|
7
|
+
ProductDetailControllerInput as Input,
|
|
8
|
+
} from "../../../.finesoft/controller-types";
|
|
9
9
|
|
|
10
|
-
export class ProductDetailController extends
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
async execute(params: { id: number }, container: Container): Promise<ProductPage> {
|
|
14
|
-
const loggerFactory = container.resolve<LoggerFactory>(DEP_KEYS.LOGGER_FACTORY);
|
|
10
|
+
export class ProductDetailController extends BaseServerController<Input, ProductPage> {
|
|
11
|
+
async execute({ params, context }: Input): Promise<ProductPage> {
|
|
12
|
+
const loggerFactory = await context.get<LoggerFactory>(DEP_KEYS.LOGGER_FACTORY);
|
|
15
13
|
const log: Logger = loggerFactory.loggerFor("ProductDetailController");
|
|
16
14
|
log.info(`Loading product ${params.id}`);
|
|
17
15
|
|
|
16
|
+
// Application-owned browsing preference, unrelated to authentication.
|
|
17
|
+
// getCookie reads the incoming request; setCookie only writes the response.
|
|
18
|
+
context.responseHeaders.set("cache-control", "private, no-store");
|
|
19
|
+
if (context.getCookie("last_product") !== String(params.id)) {
|
|
20
|
+
context.setCookie("last_product", String(params.id), {
|
|
21
|
+
httpOnly: true,
|
|
22
|
+
sameSite: "Lax",
|
|
23
|
+
secure: new URL(context.request.url).protocol === "https:",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
// Simulated API fetch — replace with real HttpClient call
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
return markPublic(
|
|
29
|
+
{
|
|
30
|
+
id: `product-${params.id}`,
|
|
31
|
+
pageType: "product",
|
|
32
|
+
title: `Product ${params.id}`,
|
|
33
|
+
description: `Details for product ${params.id}`,
|
|
34
|
+
url: `/products/${params.id}`,
|
|
35
|
+
product: {
|
|
36
|
+
id: String(params.id),
|
|
37
|
+
name: `Product ${params.id}`,
|
|
38
|
+
price: 29.99,
|
|
39
|
+
description:
|
|
40
|
+
"This is a demo product showcasing route-inferred params and DI container usage.",
|
|
41
|
+
imageUrl: `/img/product-${params.id}.svg`,
|
|
42
|
+
},
|
|
32
43
|
},
|
|
33
|
-
|
|
44
|
+
{
|
|
45
|
+
product: {
|
|
46
|
+
id: true,
|
|
47
|
+
name: true,
|
|
48
|
+
price: true,
|
|
49
|
+
description: true,
|
|
50
|
+
imageUrl: true,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
);
|
|
34
54
|
}
|
|
35
55
|
|
|
36
|
-
fallback(
|
|
37
|
-
return
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
fallback({ params }: Failure): ProductPage {
|
|
57
|
+
return markPublic(
|
|
58
|
+
{
|
|
59
|
+
id: `product-${params.id}`,
|
|
60
|
+
pageType: "product",
|
|
61
|
+
title: "Product Not Found",
|
|
62
|
+
description: "The product is currently unavailable.",
|
|
63
|
+
url: `/products/${params.id}`,
|
|
64
|
+
product: {
|
|
65
|
+
id: String(params.id),
|
|
66
|
+
name: "Unknown Product",
|
|
67
|
+
price: 0,
|
|
68
|
+
description: "Could not load product data.",
|
|
69
|
+
imageUrl: "/img/placeholder.svg",
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
product: {
|
|
74
|
+
id: true,
|
|
75
|
+
name: true,
|
|
76
|
+
price: true,
|
|
77
|
+
description: true,
|
|
78
|
+
imageUrl: true,
|
|
79
|
+
},
|
|
49
80
|
},
|
|
50
|
-
|
|
81
|
+
);
|
|
51
82
|
}
|
|
52
83
|
}
|
|
@@ -1,58 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { PRODUCTS } from "../data/products";
|
|
2
|
+
import { BaseController } from "@finesoft/front";
|
|
3
|
+
import { markPublic } from "@finesoft/front";
|
|
4
|
+
import type { SearchPage } from "../models/product";
|
|
5
|
+
import type { SearchControllerInput as Input } from "../../../.finesoft/controller-types";
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
{
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
];
|
|
7
|
+
export class SearchController extends BaseController<Input, SearchPage> {
|
|
8
|
+
execute({ query }: Input): SearchPage {
|
|
9
|
+
const term = query.q;
|
|
10
|
+
const results = term
|
|
11
|
+
? PRODUCTS.filter((p) => p.name.toLowerCase().includes(term.toLowerCase()))
|
|
12
|
+
: PRODUCTS;
|
|
38
13
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
14
|
+
return markPublic(
|
|
15
|
+
{
|
|
16
|
+
id: "search",
|
|
17
|
+
pageType: "search",
|
|
18
|
+
title: term ? `Search: ${term}` : "All Products",
|
|
19
|
+
description: `${results.length} result(s)`,
|
|
20
|
+
url: "/search",
|
|
21
|
+
query: term,
|
|
22
|
+
results,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
query: true,
|
|
26
|
+
results: {
|
|
27
|
+
id: true,
|
|
28
|
+
itemType: true,
|
|
29
|
+
name: true,
|
|
30
|
+
price: true,
|
|
31
|
+
imageUrl: true,
|
|
32
|
+
clickAction: { kind: true, url: true },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
);
|
|
57
36
|
}
|
|
58
37
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ProductItem } from "../models/product";
|
|
2
|
+
|
|
3
|
+
export const PRODUCTS: ProductItem[] = [
|
|
4
|
+
{
|
|
5
|
+
id: "1",
|
|
6
|
+
itemType: "product",
|
|
7
|
+
name: "TypeScript Handbook",
|
|
8
|
+
price: 29.99,
|
|
9
|
+
imageUrl: "/img/ts.svg",
|
|
10
|
+
clickAction: { url: "/products/1" },
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: "2",
|
|
14
|
+
itemType: "product",
|
|
15
|
+
name: "Vite Starter Kit",
|
|
16
|
+
price: 19.99,
|
|
17
|
+
imageUrl: "/img/vite.svg",
|
|
18
|
+
clickAction: { url: "/products/2" },
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "3",
|
|
22
|
+
itemType: "product",
|
|
23
|
+
name: "Hono Framework Guide",
|
|
24
|
+
price: 24.99,
|
|
25
|
+
imageUrl: "/img/hono.svg",
|
|
26
|
+
clickAction: { url: "/products/3" },
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "4",
|
|
30
|
+
itemType: "product",
|
|
31
|
+
name: "SSR Deep Dive",
|
|
32
|
+
price: 34.99,
|
|
33
|
+
imageUrl: "/img/ssr.svg",
|
|
34
|
+
clickAction: { url: "/products/4" },
|
|
35
|
+
},
|
|
36
|
+
];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mapEach, type Mapper } from "@finesoft/front";
|
|
2
2
|
import type { ProductItem } from "../models/product";
|
|
3
3
|
|
|
4
4
|
/** Raw API response shape */
|
|
@@ -16,12 +16,12 @@ const toProductItem: Mapper<ApiProduct, ProductItem> = (raw) => ({
|
|
|
16
16
|
name: raw.title,
|
|
17
17
|
price: raw.price,
|
|
18
18
|
imageUrl: raw.thumbnail ?? "/img/placeholder.svg",
|
|
19
|
-
clickAction:
|
|
19
|
+
clickAction: { url: `/products/${raw.id}` },
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Composed mapper pipeline: ApiProduct[] → ProductItem[]
|
|
24
24
|
*
|
|
25
|
-
* Demonstrates
|
|
25
|
+
* Demonstrates mapEach() for data transformation.
|
|
26
26
|
*/
|
|
27
27
|
export const mapProducts: Mapper<ApiProduct[], ProductItem[]> = mapEach(toProductItem);
|
|
@@ -1,15 +1,25 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { BasePage } from "@finesoft/front";
|
|
2
|
+
interface BaseItem {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
}
|
|
5
|
+
interface BaseShelf {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly title: string;
|
|
8
|
+
}
|
|
2
9
|
|
|
3
10
|
export interface ProductItem extends BaseItem {
|
|
4
11
|
itemType: "product";
|
|
5
12
|
name: string;
|
|
6
13
|
price: number;
|
|
7
14
|
imageUrl: string;
|
|
15
|
+
clickAction?: { readonly url: string };
|
|
8
16
|
}
|
|
9
17
|
|
|
10
18
|
export interface ProductShelf extends BaseShelf {
|
|
11
19
|
shelfType: "products";
|
|
12
20
|
items: ProductItem[];
|
|
21
|
+
isHorizontal?: boolean;
|
|
22
|
+
seeAllAction?: { readonly url: string };
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
export interface HomePage extends BasePage {
|
|
@@ -6,11 +6,14 @@ import { HttpClient, type HttpClientConfig } from "@finesoft/front";
|
|
|
6
6
|
* Showcases HttpClient subclass pattern with base URL and custom fetch.
|
|
7
7
|
*/
|
|
8
8
|
export class ApiClient extends HttpClient {
|
|
9
|
-
constructor(config
|
|
9
|
+
constructor(config: Partial<HttpClientConfig> & Pick<HttpClientConfig, "fetch">) {
|
|
10
10
|
super({
|
|
11
|
-
baseUrl: config
|
|
12
|
-
defaultHeaders: { "Content-Type": "application/json", ...config
|
|
13
|
-
fetch: config
|
|
11
|
+
baseUrl: config.baseUrl ?? "/api",
|
|
12
|
+
defaultHeaders: { "Content-Type": "application/json", ...config.defaultHeaders },
|
|
13
|
+
fetch: config.fetch,
|
|
14
|
+
validateDns: config.validateDns,
|
|
15
|
+
lookup: config.lookup,
|
|
16
|
+
allowInternalHosts: config.allowInternalHosts,
|
|
14
17
|
});
|
|
15
18
|
}
|
|
16
19
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createBrowserApp } from "@finesoft/front";
|
|
2
|
+
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
3
|
+
import { createElement } from "react";
|
|
4
|
+
import { app } from "./app-definition";
|
|
5
|
+
import App from "./App";
|
|
6
|
+
import "./styles.css";
|
|
7
|
+
|
|
8
|
+
const target = document.getElementById("app")!;
|
|
9
|
+
export async function mountApplication(
|
|
10
|
+
target: HTMLElement,
|
|
11
|
+
history: "browser" | "memory" = "browser",
|
|
12
|
+
url?: string,
|
|
13
|
+
) {
|
|
14
|
+
const handle = await createBrowserApp({ definition: app, target, history, url });
|
|
15
|
+
const root = handle.shouldHydrate
|
|
16
|
+
? hydrateRoot(target, createElement(App, { app: handle }))
|
|
17
|
+
: createRoot(target);
|
|
18
|
+
if (!handle.shouldHydrate) root.render(createElement(App, { app: handle }));
|
|
19
|
+
const originalDispose = handle.dispose.bind(handle);
|
|
20
|
+
let disposal: Promise<void> | undefined;
|
|
21
|
+
Object.assign(handle, {
|
|
22
|
+
dispose: () => (disposal ??= originalDispose().finally(() => root.unmount())),
|
|
23
|
+
});
|
|
24
|
+
await handle.ready;
|
|
25
|
+
return handle;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const started = mountApplication(target);
|
|
29
|
+
|
|
30
|
+
if (import.meta.hot) {
|
|
31
|
+
import.meta.hot.dispose(async () => {
|
|
32
|
+
await (await started).dispose();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import type { Action } from "@finesoft/front";
|
|
2
1
|
import type { AboutPage } from "../lib/models/product";
|
|
3
2
|
|
|
4
3
|
interface AboutProps {
|
|
5
4
|
page: AboutPage;
|
|
6
|
-
onAction?: (action: Action) => void;
|
|
7
5
|
}
|
|
8
6
|
|
|
9
7
|
export default function About({ page }: AboutProps) {
|
|
10
8
|
return (
|
|
11
|
-
<
|
|
9
|
+
<section className="page page-about">
|
|
12
10
|
<h1>{page.title}</h1>
|
|
13
11
|
<p>{page.content}</p>
|
|
14
|
-
</
|
|
12
|
+
</section>
|
|
15
13
|
);
|
|
16
14
|
}
|
|
@@ -1,34 +1,25 @@
|
|
|
1
|
-
import type { Action } from "@finesoft/front";
|
|
2
1
|
import ProductCard from "../components/ProductCard";
|
|
3
2
|
import type { HomePage } from "../lib/models/product";
|
|
4
3
|
|
|
5
4
|
interface HomeProps {
|
|
6
5
|
page: HomePage;
|
|
7
|
-
onAction?: (action: Action) => void;
|
|
8
6
|
}
|
|
9
7
|
|
|
10
|
-
export default function Home({ page
|
|
8
|
+
export default function Home({ page }: HomeProps) {
|
|
11
9
|
return (
|
|
12
|
-
<
|
|
10
|
+
<section className="page page-home">
|
|
13
11
|
<h1>{page.title}</h1>
|
|
14
12
|
<p>{page.description}</p>
|
|
15
13
|
{page.shelves.map((shelf) => (
|
|
16
|
-
<section key={shelf.id}>
|
|
14
|
+
<section className="shelf-section" key={shelf.id}>
|
|
17
15
|
<h2>{shelf.title}</h2>
|
|
18
|
-
<div
|
|
19
|
-
style={{
|
|
20
|
-
display: "flex",
|
|
21
|
-
gap: "1rem",
|
|
22
|
-
flexWrap: shelf.isHorizontal ? "nowrap" : "wrap",
|
|
23
|
-
overflowX: shelf.isHorizontal ? "auto" : undefined,
|
|
24
|
-
}}
|
|
25
|
-
>
|
|
16
|
+
<div className={`shelf${shelf.isHorizontal ? " horizontal" : ""}`}>
|
|
26
17
|
{shelf.items.map((item) => (
|
|
27
|
-
<ProductCard key={item.id} item={item}
|
|
18
|
+
<ProductCard key={item.id} item={item} />
|
|
28
19
|
))}
|
|
29
20
|
</div>
|
|
30
21
|
</section>
|
|
31
22
|
))}
|
|
32
|
-
</
|
|
23
|
+
</section>
|
|
33
24
|
);
|
|
34
25
|
}
|
|
@@ -1,25 +1,15 @@
|
|
|
1
|
-
import type { Action } from "@finesoft/front";
|
|
2
|
-
import { makeFlowAction } from "@finesoft/front";
|
|
3
1
|
import type { ErrorPage } from "../lib/models/product";
|
|
4
2
|
|
|
5
3
|
interface NotFoundProps {
|
|
6
4
|
page: ErrorPage;
|
|
7
|
-
onAction?: (action: Action) => void;
|
|
8
5
|
}
|
|
9
6
|
|
|
10
|
-
export default function NotFound({ page
|
|
11
|
-
const handleHome = (e: React.MouseEvent) => {
|
|
12
|
-
e.preventDefault();
|
|
13
|
-
onAction?.(makeFlowAction("/"));
|
|
14
|
-
};
|
|
15
|
-
|
|
7
|
+
export default function NotFound({ page }: NotFoundProps) {
|
|
16
8
|
return (
|
|
17
|
-
<
|
|
9
|
+
<section className="page page-error">
|
|
18
10
|
<h1>{page.title}</h1>
|
|
19
11
|
<p>{page.description}</p>
|
|
20
|
-
<a href="/"
|
|
21
|
-
|
|
22
|
-
</a>
|
|
23
|
-
</div>
|
|
12
|
+
<a href="/">← Go Home</a>
|
|
13
|
+
</section>
|
|
24
14
|
);
|
|
25
15
|
}
|
|
@@ -1,34 +1,16 @@
|
|
|
1
|
-
import type { Action } from "@finesoft/front";
|
|
2
|
-
import { makeFlowAction } from "@finesoft/front";
|
|
3
1
|
import type { ProductPage } from "../lib/models/product";
|
|
4
2
|
|
|
5
3
|
interface ProductDetailProps {
|
|
6
4
|
page: ProductPage;
|
|
7
|
-
onAction?: (action: Action) => void;
|
|
8
5
|
}
|
|
9
6
|
|
|
10
|
-
export default function ProductDetail({ page
|
|
11
|
-
const handleBack = (e: React.MouseEvent) => {
|
|
12
|
-
e.preventDefault();
|
|
13
|
-
onAction?.(makeFlowAction("/"));
|
|
14
|
-
};
|
|
15
|
-
|
|
7
|
+
export default function ProductDetail({ page }: ProductDetailProps) {
|
|
16
8
|
return (
|
|
17
|
-
<
|
|
18
|
-
<a href="/"
|
|
19
|
-
← Back
|
|
20
|
-
</a>
|
|
9
|
+
<section className="page page-product">
|
|
10
|
+
<a href="/">← Back</a>
|
|
21
11
|
<h1>{page.product.name}</h1>
|
|
22
|
-
<p
|
|
23
|
-
style={{
|
|
24
|
-
fontSize: "1.5rem",
|
|
25
|
-
color: "#007bff",
|
|
26
|
-
fontWeight: "bold",
|
|
27
|
-
}}
|
|
28
|
-
>
|
|
29
|
-
${page.product.price.toFixed(2)}
|
|
30
|
-
</p>
|
|
12
|
+
<p className="price price-large">${page.product.price.toFixed(2)}</p>
|
|
31
13
|
<p>{page.product.description}</p>
|
|
32
|
-
</
|
|
14
|
+
</section>
|
|
33
15
|
);
|
|
34
16
|
}
|
|
@@ -1,23 +1,21 @@
|
|
|
1
|
-
import type { Action } from "@finesoft/front";
|
|
2
1
|
import ProductCard from "../components/ProductCard";
|
|
3
2
|
import type { SearchPage } from "../lib/models/product";
|
|
4
3
|
|
|
5
4
|
interface SearchProps {
|
|
6
5
|
page: SearchPage;
|
|
7
|
-
onAction?: (action: Action) => void;
|
|
8
6
|
}
|
|
9
7
|
|
|
10
|
-
export default function Search({ page
|
|
8
|
+
export default function Search({ page }: SearchProps) {
|
|
11
9
|
return (
|
|
12
|
-
<
|
|
10
|
+
<section className="page page-search">
|
|
13
11
|
<h1>{page.title}</h1>
|
|
14
12
|
<p>{page.description}</p>
|
|
15
|
-
<div
|
|
13
|
+
<div className="results">
|
|
16
14
|
{page.results.map((item) => (
|
|
17
|
-
<ProductCard key={item.id} item={item}
|
|
15
|
+
<ProductCard key={item.id} item={item} />
|
|
18
16
|
))}
|
|
19
17
|
</div>
|
|
20
18
|
{page.results.length === 0 && <p>No products found.</p>}
|
|
21
|
-
</
|
|
19
|
+
</section>
|
|
22
20
|
);
|
|
23
21
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { renderToString } from "react-dom/server";
|
|
2
|
+
import { createElement } from "react";
|
|
3
|
+
import { createSSRRender } from "@finesoft/front";
|
|
4
|
+
import { app } from "./app-definition";
|
|
5
|
+
import App from "./App";
|
|
6
|
+
|
|
7
|
+
export const render = createSSRRender({
|
|
8
|
+
definition: app,
|
|
9
|
+
render: (app) => renderToString(createElement(App, { app })),
|
|
10
|
+
});
|
|
11
|
+
export { serializeServerData } from "@finesoft/front";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color: #333;
|
|
3
|
+
font-family: Arial, sans-serif;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
body {
|
|
7
|
+
margin: 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.app-shell {
|
|
11
|
+
min-height: 100vh;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.navigation {
|
|
15
|
+
display: flex;
|
|
16
|
+
gap: 1rem;
|
|
17
|
+
padding: 1rem;
|
|
18
|
+
border-bottom: 1px solid #eee;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.navigation-link {
|
|
22
|
+
color: #333;
|
|
23
|
+
text-decoration: none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.navigation-link:hover,
|
|
27
|
+
.navigation-link.active {
|
|
28
|
+
color: #007bff;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.navigation-link.active {
|
|
32
|
+
font-weight: bold;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.app-main {
|
|
36
|
+
padding: 1rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.loading {
|
|
40
|
+
padding: 2rem;
|
|
41
|
+
color: #999;
|
|
42
|
+
text-align: center;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.shelf,
|
|
46
|
+
.results {
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-wrap: wrap;
|
|
49
|
+
gap: 1rem;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.shelf.horizontal {
|
|
53
|
+
overflow-x: auto;
|
|
54
|
+
flex-wrap: nowrap;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.product-card {
|
|
58
|
+
min-width: 200px;
|
|
59
|
+
padding: 1rem;
|
|
60
|
+
border: 1px solid #eee;
|
|
61
|
+
border-radius: 8px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.price {
|
|
65
|
+
color: #007bff;
|
|
66
|
+
font-weight: bold;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.price-large {
|
|
70
|
+
font-size: 1.5rem;
|
|
71
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import About from "./pages/About";
|
|
2
|
+
import Home from "./pages/Home";
|
|
3
|
+
import NotFound from "./pages/NotFound";
|
|
4
|
+
import ProductDetail from "./pages/ProductDetail";
|
|
5
|
+
import Search from "./pages/Search";
|
|
6
|
+
export const views = {
|
|
7
|
+
home: Home,
|
|
8
|
+
product: ProductDetail,
|
|
9
|
+
search: Search,
|
|
10
|
+
about: About,
|
|
11
|
+
error: NotFound,
|
|
12
|
+
};
|
|
@@ -4,20 +4,25 @@
|
|
|
4
4
|
"target": "ESNext",
|
|
5
5
|
"module": "ESNext",
|
|
6
6
|
"moduleResolution": "bundler",
|
|
7
|
-
"
|
|
7
|
+
"allowImportingTsExtensions": true,
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": true,
|
|
10
|
+
"sourceMap": true,
|
|
8
11
|
"esModuleInterop": true,
|
|
9
12
|
"forceConsistentCasingInFileNames": true,
|
|
10
13
|
"skipLibCheck": true,
|
|
11
14
|
"noEmit": true,
|
|
15
|
+
"rootDir": ".",
|
|
16
|
+
"jsx": "react-jsx",
|
|
12
17
|
"paths": {
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"@finesoft/core": ["../../packages/core/src/index.ts"],
|
|
17
|
-
"@finesoft/browser": ["../../packages/browser/src/index.ts"],
|
|
18
|
-
"@finesoft/ssr": ["../../packages/ssr/src/index.ts"],
|
|
19
|
-
"@finesoft/server": ["../../packages/server/src/index.ts"]
|
|
18
|
+
"@finesoft/front": [
|
|
19
|
+
"./.finesoft/front.d.ts"
|
|
20
|
+
]
|
|
20
21
|
}
|
|
21
22
|
},
|
|
22
|
-
"include": [
|
|
23
|
+
"include": [
|
|
24
|
+
"src/**/*",
|
|
25
|
+
"env.d.ts",
|
|
26
|
+
"vite.config.ts"
|
|
27
|
+
]
|
|
23
28
|
}
|