@finesoft/create-app 0.1.34 → 0.1.36
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,36 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
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";
|
|
1
|
+
import { PRODUCTS } from "../data/products";
|
|
2
|
+
import { BaseController, type ControllerInput } from "@finesoft/front";
|
|
3
|
+
import { markPublic, makeFlowAction } from "@finesoft/front";
|
|
4
|
+
import type { HomePage, ProductShelf } from "../models/product";
|
|
33
5
|
|
|
6
|
+
export class HomeController extends BaseController<
|
|
7
|
+
ControllerInput<Record<string, string>>,
|
|
8
|
+
HomePage
|
|
9
|
+
> {
|
|
34
10
|
execute(): HomePage {
|
|
35
11
|
const shelf: ProductShelf = {
|
|
36
12
|
id: "featured",
|
|
@@ -38,16 +14,35 @@ export class HomeController extends BaseController<Record<string, string>, HomeP
|
|
|
38
14
|
title: "Featured Products",
|
|
39
15
|
isHorizontal: true,
|
|
40
16
|
seeAllAction: makeFlowAction("/search"),
|
|
41
|
-
items:
|
|
17
|
+
items: PRODUCTS.slice(0, 3),
|
|
42
18
|
};
|
|
43
19
|
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
20
|
+
return markPublic(
|
|
21
|
+
{
|
|
22
|
+
id: "home",
|
|
23
|
+
pageType: "home",
|
|
24
|
+
title: "Home",
|
|
25
|
+
description: "Welcome to Finesoft Front Demo",
|
|
26
|
+
url: "/",
|
|
27
|
+
shelves: [shelf],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
shelves: {
|
|
31
|
+
id: true,
|
|
32
|
+
shelfType: true,
|
|
33
|
+
title: true,
|
|
34
|
+
isHorizontal: true,
|
|
35
|
+
seeAllAction: { kind: true, url: true },
|
|
36
|
+
items: {
|
|
37
|
+
id: true,
|
|
38
|
+
itemType: true,
|
|
39
|
+
name: true,
|
|
40
|
+
price: true,
|
|
41
|
+
imageUrl: true,
|
|
42
|
+
clickAction: { kind: true, url: true },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
);
|
|
52
47
|
}
|
|
53
48
|
}
|
|
@@ -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 {
|
|
@@ -44,4 +54,5 @@ export interface ErrorPage extends BasePage {
|
|
|
44
54
|
status: number;
|
|
45
55
|
}
|
|
46
56
|
|
|
57
|
+
/** 所有页面类型联合 */
|
|
47
58
|
export type AppPage = HomePage | ProductPage | SearchPage | AboutPage | ErrorPage;
|
|
@@ -6,14 +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: {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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,
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -1,46 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createApp,
|
|
1
|
+
import { createBrowserApp } from "@finesoft/front";
|
|
2
|
+
import { createApp, createSSRApp } from "vue";
|
|
3
|
+
import { app } from "./app-definition";
|
|
3
4
|
import App from "./App.vue";
|
|
4
|
-
import
|
|
5
|
-
import type { AppPage } from "./lib/models/product";
|
|
5
|
+
import "./styles.css";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
const target = document.getElementById("app")!;
|
|
8
|
+
export async function mountApplication(
|
|
9
|
+
target: HTMLElement,
|
|
10
|
+
history: "browser" | "memory" = "browser",
|
|
11
|
+
url?: string,
|
|
12
|
+
) {
|
|
13
|
+
const handle = await createBrowserApp({ definition: app, target, history, url });
|
|
14
|
+
const root = (handle.shouldHydrate ? createSSRApp : createApp)(App, { app: handle });
|
|
15
|
+
root.mount(target);
|
|
16
|
+
const originalDispose = handle.dispose.bind(handle);
|
|
17
|
+
let disposal: Promise<void> | undefined;
|
|
18
|
+
Object.assign(handle, {
|
|
19
|
+
dispose: () => (disposal ??= originalDispose().finally(() => root.unmount())),
|
|
20
|
+
});
|
|
21
|
+
await handle.ready;
|
|
22
|
+
return handle;
|
|
23
|
+
}
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (page instanceof Promise) {
|
|
27
|
-
if (!isFirstPage) state.loading = true;
|
|
28
|
-
void page.then((p) => {
|
|
29
|
-
state.page = p as AppPage;
|
|
30
|
-
state.loading = false;
|
|
31
|
-
});
|
|
32
|
-
} else {
|
|
33
|
-
state.page = page as AppPage;
|
|
34
|
-
state.loading = false;
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
},
|
|
38
|
-
callbacks: {
|
|
39
|
-
onNavigate(pathname) {
|
|
40
|
-
console.log("[navigate]", pathname);
|
|
41
|
-
},
|
|
42
|
-
onModal(page) {
|
|
43
|
-
console.log("[modal]", page.title);
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
});
|
|
25
|
+
export const started = mountApplication(target);
|
|
26
|
+
|
|
27
|
+
if (import.meta.hot) {
|
|
28
|
+
import.meta.hot.dispose(async () => {
|
|
29
|
+
await (await started).dispose();
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { Action } from "@finesoft/front";
|
|
3
2
|
import type { AboutPage } from "../lib/models/product";
|
|
4
3
|
|
|
5
|
-
const { page
|
|
4
|
+
const { page } = defineProps<{
|
|
6
5
|
page: AboutPage;
|
|
7
|
-
onAction?: (action: Action) => void;
|
|
8
6
|
}>();
|
|
9
7
|
</script>
|
|
10
8
|
|
|
11
9
|
<template>
|
|
12
|
-
<
|
|
10
|
+
<section class="page page-about">
|
|
13
11
|
<h1>{{ page.title }}</h1>
|
|
14
12
|
<p>{{ page.content }}</p>
|
|
15
|
-
</
|
|
13
|
+
</section>
|
|
16
14
|
</template>
|
|
@@ -1,38 +1,20 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { Action } from "@finesoft/front";
|
|
3
2
|
import ProductCard from "../components/ProductCard.vue";
|
|
4
3
|
import type { HomePage } from "../lib/models/product";
|
|
5
4
|
|
|
6
|
-
const { page
|
|
5
|
+
const { page } = defineProps<{ page: HomePage }>();
|
|
7
6
|
</script>
|
|
8
7
|
|
|
9
8
|
<template>
|
|
10
|
-
<
|
|
9
|
+
<section class="page page-home">
|
|
11
10
|
<h1>{{ page.title }}</h1>
|
|
12
11
|
<p>{{ page.description }}</p>
|
|
13
12
|
|
|
14
|
-
<section v-for="shelf in page.shelves" :key="shelf.id">
|
|
13
|
+
<section v-for="shelf in page.shelves" :key="shelf.id" class="shelf-section">
|
|
15
14
|
<h2>{{ shelf.title }}</h2>
|
|
16
15
|
<div class="shelf" :class="{ horizontal: shelf.isHorizontal }">
|
|
17
|
-
<ProductCard
|
|
18
|
-
v-for="item in shelf.items"
|
|
19
|
-
:key="item.id"
|
|
20
|
-
:item="item"
|
|
21
|
-
:on-action="onAction"
|
|
22
|
-
/>
|
|
16
|
+
<ProductCard v-for="item in shelf.items" :key="item.id" :item="item" />
|
|
23
17
|
</div>
|
|
24
18
|
</section>
|
|
25
|
-
</
|
|
19
|
+
</section>
|
|
26
20
|
</template>
|
|
27
|
-
|
|
28
|
-
<style scoped>
|
|
29
|
-
.shelf {
|
|
30
|
-
display: flex;
|
|
31
|
-
gap: 1rem;
|
|
32
|
-
flex-wrap: wrap;
|
|
33
|
-
}
|
|
34
|
-
.shelf.horizontal {
|
|
35
|
-
flex-wrap: nowrap;
|
|
36
|
-
overflow-x: auto;
|
|
37
|
-
}
|
|
38
|
-
</style>
|
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { Action } from "@finesoft/front";
|
|
3
|
-
import { NAV_ACTIONS } from "../actions";
|
|
4
2
|
import type { ErrorPage } from "../lib/models/product";
|
|
5
3
|
|
|
6
|
-
const { page
|
|
7
|
-
|
|
8
|
-
function handleHome(e: MouseEvent) {
|
|
9
|
-
if (onAction) {
|
|
10
|
-
e.preventDefault();
|
|
11
|
-
onAction(NAV_ACTIONS.home);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
4
|
+
const { page } = defineProps<{ page: ErrorPage }>();
|
|
14
5
|
</script>
|
|
15
6
|
|
|
16
7
|
<template>
|
|
17
|
-
<
|
|
8
|
+
<section class="page page-error">
|
|
18
9
|
<h1>{{ page.title }}</h1>
|
|
19
10
|
<p>{{ page.description }}</p>
|
|
20
|
-
<a href="/"
|
|
21
|
-
</
|
|
11
|
+
<a href="/">← Go Home</a>
|
|
12
|
+
</section>
|
|
22
13
|
</template>
|
|
@@ -1,34 +1,16 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { Action } from "@finesoft/front";
|
|
3
|
-
import { NAV_ACTIONS } from "../actions";
|
|
4
2
|
import type { ProductPage } from "../lib/models/product";
|
|
5
3
|
|
|
6
|
-
const { page
|
|
4
|
+
const { page } = defineProps<{
|
|
7
5
|
page: ProductPage;
|
|
8
|
-
onAction?: (action: Action) => void;
|
|
9
6
|
}>();
|
|
10
|
-
|
|
11
|
-
function handleBack(e: MouseEvent) {
|
|
12
|
-
if (onAction) {
|
|
13
|
-
e.preventDefault();
|
|
14
|
-
onAction(NAV_ACTIONS.home);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
7
|
</script>
|
|
18
8
|
|
|
19
9
|
<template>
|
|
20
|
-
<
|
|
21
|
-
<a href="/"
|
|
10
|
+
<section class="page page-product">
|
|
11
|
+
<a href="/">← Back</a>
|
|
22
12
|
<h1>{{ page.product.name }}</h1>
|
|
23
|
-
<p class="price">${{ page.product.price.toFixed(2) }}</p>
|
|
13
|
+
<p class="price price-large">${{ page.product.price.toFixed(2) }}</p>
|
|
24
14
|
<p>{{ page.product.description }}</p>
|
|
25
|
-
</
|
|
15
|
+
</section>
|
|
26
16
|
</template>
|
|
27
|
-
|
|
28
|
-
<style scoped>
|
|
29
|
-
.price {
|
|
30
|
-
font-size: 1.5rem;
|
|
31
|
-
color: #007bff;
|
|
32
|
-
font-weight: bold;
|
|
33
|
-
}
|
|
34
|
-
</style>
|