@reactionary/source 0.0.30 → 0.0.32

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 (119) hide show
  1. package/CLAUDE.md +11 -0
  2. package/core/package.json +1 -1
  3. package/core/src/cache/cache-evaluation.interface.ts +19 -0
  4. package/core/src/cache/cache.interface.ts +38 -0
  5. package/core/src/cache/noop-cache.ts +42 -0
  6. package/core/src/cache/redis-cache.ts +55 -22
  7. package/core/src/client/client-builder.ts +63 -0
  8. package/core/src/client/client.ts +27 -3
  9. package/core/src/decorators/trpc.decorators.ts +144 -0
  10. package/core/src/index.ts +6 -1
  11. package/core/src/providers/analytics.provider.ts +3 -6
  12. package/core/src/providers/base.provider.ts +13 -63
  13. package/core/src/providers/cart.provider.ts +10 -6
  14. package/core/src/providers/identity.provider.ts +8 -5
  15. package/core/src/providers/inventory.provider.ts +5 -6
  16. package/core/src/providers/price.provider.ts +6 -6
  17. package/core/src/providers/product.provider.ts +6 -6
  18. package/core/src/providers/search.provider.ts +6 -6
  19. package/core/src/schemas/mutations/base.mutation.ts +0 -1
  20. package/core/src/schemas/mutations/cart.mutation.ts +0 -6
  21. package/core/src/schemas/mutations/identity.mutation.ts +0 -5
  22. package/core/src/schemas/mutations/product.mutation.ts +0 -1
  23. package/core/src/schemas/queries/base.query.ts +0 -1
  24. package/core/src/schemas/queries/cart.query.ts +1 -3
  25. package/core/src/schemas/queries/identity.query.ts +1 -3
  26. package/core/src/schemas/queries/inventory.query.ts +0 -1
  27. package/core/src/schemas/queries/price.query.ts +0 -3
  28. package/core/src/schemas/queries/product.query.ts +2 -7
  29. package/core/src/schemas/queries/search.query.ts +0 -3
  30. package/examples/node/package.json +1 -5
  31. package/examples/node/src/basic/basic-node-provider-model-extension.spec.ts +97 -0
  32. package/examples/node/src/basic/basic-node-provider-query-extension.spec.ts +84 -0
  33. package/examples/node/src/basic/basic-node-setup.spec.ts +40 -0
  34. package/otel/src/index.ts +3 -0
  35. package/otel/src/trace-decorator.ts +246 -0
  36. package/package.json +2 -1
  37. package/providers/algolia/src/core/initialize.ts +11 -9
  38. package/providers/algolia/src/providers/product.provider.ts +44 -11
  39. package/providers/algolia/src/providers/search.provider.ts +47 -66
  40. package/providers/commercetools/src/core/client.ts +0 -1
  41. package/providers/commercetools/src/core/initialize.ts +28 -24
  42. package/providers/commercetools/src/providers/cart.provider.ts +58 -89
  43. package/providers/commercetools/src/providers/identity.provider.ts +34 -50
  44. package/providers/commercetools/src/providers/inventory.provider.ts +16 -38
  45. package/providers/commercetools/src/providers/price.provider.ts +30 -35
  46. package/providers/commercetools/src/providers/product.provider.ts +48 -38
  47. package/providers/commercetools/src/providers/search.provider.ts +32 -47
  48. package/providers/commercetools/src/schema/capabilities.schema.ts +1 -1
  49. package/providers/fake/package.json +1 -0
  50. package/providers/fake/src/core/initialize.ts +17 -14
  51. package/providers/fake/src/index.ts +4 -0
  52. package/providers/fake/src/providers/analytics.provider.ts +19 -0
  53. package/providers/fake/src/providers/cart.provider.ts +107 -0
  54. package/providers/fake/src/providers/identity.provider.ts +78 -67
  55. package/providers/fake/src/providers/inventory.provider.ts +54 -0
  56. package/providers/fake/src/providers/price.provider.ts +60 -0
  57. package/providers/fake/src/providers/product.provider.ts +53 -49
  58. package/providers/fake/src/providers/search.provider.ts +15 -33
  59. package/providers/posthog/src/core/initialize.ts +6 -4
  60. package/trpc/__mocks__/superjson.js +25 -0
  61. package/trpc/jest.config.ts +14 -0
  62. package/trpc/package.json +2 -1
  63. package/trpc/src/client.ts +176 -0
  64. package/trpc/src/index.ts +35 -62
  65. package/trpc/src/integration.spec.ts +216 -0
  66. package/trpc/src/server.ts +123 -0
  67. package/trpc/src/transparent-client.spec.ts +160 -0
  68. package/trpc/src/types.ts +142 -0
  69. package/trpc/tsconfig.json +3 -0
  70. package/trpc/tsconfig.lib.json +2 -1
  71. package/trpc/tsconfig.spec.json +15 -0
  72. package/tsconfig.base.json +0 -2
  73. package/core/src/cache/caching-strategy.ts +0 -25
  74. package/examples/angular/e2e/example.spec.ts +0 -9
  75. package/examples/angular/eslint.config.mjs +0 -41
  76. package/examples/angular/playwright.config.ts +0 -38
  77. package/examples/angular/project.json +0 -86
  78. package/examples/angular/public/favicon.ico +0 -0
  79. package/examples/angular/src/app/app.component.html +0 -6
  80. package/examples/angular/src/app/app.component.scss +0 -22
  81. package/examples/angular/src/app/app.component.ts +0 -14
  82. package/examples/angular/src/app/app.config.ts +0 -16
  83. package/examples/angular/src/app/app.routes.ts +0 -25
  84. package/examples/angular/src/app/cart/cart.component.html +0 -4
  85. package/examples/angular/src/app/cart/cart.component.scss +0 -14
  86. package/examples/angular/src/app/cart/cart.component.ts +0 -73
  87. package/examples/angular/src/app/identity/identity.component.html +0 -6
  88. package/examples/angular/src/app/identity/identity.component.scss +0 -18
  89. package/examples/angular/src/app/identity/identity.component.ts +0 -49
  90. package/examples/angular/src/app/product/product.component.html +0 -14
  91. package/examples/angular/src/app/product/product.component.scss +0 -11
  92. package/examples/angular/src/app/product/product.component.ts +0 -42
  93. package/examples/angular/src/app/search/search.component.html +0 -35
  94. package/examples/angular/src/app/search/search.component.scss +0 -129
  95. package/examples/angular/src/app/search/search.component.ts +0 -50
  96. package/examples/angular/src/app/services/product.service.ts +0 -35
  97. package/examples/angular/src/app/services/search.service.ts +0 -48
  98. package/examples/angular/src/app/services/trpc.client.ts +0 -27
  99. package/examples/angular/src/index.html +0 -13
  100. package/examples/angular/src/main.ts +0 -7
  101. package/examples/angular/src/styles.scss +0 -17
  102. package/examples/angular/src/test-setup.ts +0 -6
  103. package/examples/angular/tsconfig.app.json +0 -10
  104. package/examples/angular/tsconfig.editor.json +0 -6
  105. package/examples/angular/tsconfig.json +0 -32
  106. package/examples/node/src/initialize-algolia.spec.ts +0 -29
  107. package/examples/node/src/initialize-commercetools.spec.ts +0 -31
  108. package/examples/node/src/initialize-extended-providers.spec.ts +0 -38
  109. package/examples/node/src/initialize-mixed-providers.spec.ts +0 -36
  110. package/examples/node/src/providers/custom-algolia-product.provider.ts +0 -18
  111. package/examples/node/src/schemas/custom-product.schema.ts +0 -8
  112. package/examples/trpc-node/.env.example +0 -52
  113. package/examples/trpc-node/eslint.config.mjs +0 -3
  114. package/examples/trpc-node/project.json +0 -61
  115. package/examples/trpc-node/src/assets/.gitkeep +0 -0
  116. package/examples/trpc-node/src/main.ts +0 -59
  117. package/examples/trpc-node/src/router-instance.ts +0 -52
  118. package/examples/trpc-node/tsconfig.app.json +0 -9
  119. package/examples/trpc-node/tsconfig.json +0 -13
@@ -1,25 +0,0 @@
1
- import { BaseQuery } from "../schemas/queries/base.query";
2
- import { InventoryQuery } from "../schemas/queries/inventory.query";
3
- import { Session } from "../schemas/session.schema";
4
-
5
- export interface CachingStrategyEvaluation {
6
- key: string;
7
- cacheDurationInSeconds: number;
8
- canCache: boolean;
9
- }
10
-
11
- export interface CachingStrategy {
12
- get(query: BaseQuery, session: Session): CachingStrategyEvaluation;
13
- }
14
-
15
- export class BaseCachingStrategy implements CachingStrategy {
16
- public get(query: BaseQuery, session: Session): CachingStrategyEvaluation {
17
- const q = query as InventoryQuery;
18
-
19
- return {
20
- key: q.sku,
21
- cacheDurationInSeconds: 300,
22
- canCache: true
23
- }
24
- }
25
- }
@@ -1,9 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- test('displays search results', async ({ page }) => {
4
- await page.goto('/');
5
-
6
- await page.waitForSelector('article', { state: 'visible' });
7
-
8
- expect(await page.locator('article').count()).toBe(20);
9
- });
@@ -1,41 +0,0 @@
1
- import playwright from 'eslint-plugin-playwright';
2
- import nx from '@nx/eslint-plugin';
3
- import baseConfig from '../../eslint.config.mjs';
4
-
5
- export default [
6
- playwright.configs['flat/recommended'],
7
- ...baseConfig,
8
- ...nx.configs['flat/angular'],
9
- ...nx.configs['flat/angular-template'],
10
- {
11
- files: ['**/*.ts'],
12
- rules: {
13
- '@angular-eslint/directive-selector': [
14
- 'error',
15
- {
16
- type: 'attribute',
17
- prefix: 'app',
18
- style: 'camelCase',
19
- },
20
- ],
21
- '@angular-eslint/component-selector': [
22
- 'error',
23
- {
24
- type: 'element',
25
- prefix: 'app',
26
- style: 'kebab-case',
27
- },
28
- ],
29
- },
30
- },
31
- {
32
- files: ['**/*.html'],
33
- // Override or add rules here
34
- rules: {},
35
- },
36
- {
37
- files: ['**/*.ts', '**/*.js'],
38
- // Override or add rules here
39
- rules: {},
40
- },
41
- ];
@@ -1,38 +0,0 @@
1
- import { defineConfig, devices } from '@playwright/test';
2
- import { nxE2EPreset } from '@nx/playwright/preset';
3
- import { workspaceRoot } from '@nx/devkit';
4
-
5
- // For CI, you may want to set BASE_URL to the deployed application.
6
- const baseURL = process.env['BASE_URL'] || 'http://localhost:4200';
7
-
8
- /**
9
- * Read environment variables from file.
10
- * https://github.com/motdotla/dotenv
11
- */
12
- // require('dotenv').config();
13
-
14
- /**
15
- * See https://playwright.dev/docs/test-configuration.
16
- */
17
- export default defineConfig({
18
- ...nxE2EPreset(__filename, { testDir: './e2e' }),
19
- /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
20
- use: {
21
- baseURL,
22
- /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
23
- trace: 'on-first-retry',
24
- },
25
- /* Run your local dev server before starting the tests */
26
- webServer: {
27
- command: 'npx nx serve examples-angular',
28
- url: 'http://localhost:4200',
29
- reuseExistingServer: !process.env.CI,
30
- cwd: workspaceRoot,
31
- },
32
- projects: [
33
- {
34
- name: 'chromium',
35
- use: { ...devices['Desktop Chrome'] },
36
- },
37
- ],
38
- });
@@ -1,86 +0,0 @@
1
- {
2
- "name": "examples-angular",
3
- "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
- "projectType": "application",
5
- "prefix": "app",
6
- "sourceRoot": "examples/angular/src",
7
- "tags": [],
8
- "targets": {
9
- "build": {
10
- "executor": "@angular-devkit/build-angular:application",
11
- "outputs": ["{options.outputPath}"],
12
- "options": {
13
- "outputPath": "dist/examples/angular",
14
- "index": "examples/angular/src/index.html",
15
- "browser": "examples/angular/src/main.ts",
16
- "polyfills": [],
17
- "tsConfig": "examples/angular/tsconfig.app.json",
18
- "inlineStyleLanguage": "scss",
19
- "assets": [
20
- {
21
- "glob": "**/*",
22
- "input": "examples/angular/public"
23
- }
24
- ],
25
- "styles": ["examples/angular/src/styles.scss"],
26
- "scripts": []
27
- },
28
- "configurations": {
29
- "production": {
30
- "budgets": [
31
- {
32
- "type": "initial",
33
- "maximumWarning": "500kb",
34
- "maximumError": "1mb"
35
- },
36
- {
37
- "type": "anyComponentStyle",
38
- "maximumWarning": "4kb",
39
- "maximumError": "8kb"
40
- }
41
- ],
42
- "outputHashing": "all"
43
- },
44
- "development": {
45
- "optimization": false,
46
- "extractLicenses": false,
47
- "sourceMap": true
48
- }
49
- },
50
- "defaultConfiguration": "production"
51
- },
52
- "serve": {
53
- "executor": "@angular-devkit/build-angular:dev-server",
54
- "configurations": {
55
- "production": {
56
- "buildTarget": "examples-angular:build:production"
57
- },
58
- "development": {
59
- "buildTarget": "examples-angular:build:development"
60
- }
61
- },
62
- "defaultConfiguration": "development",
63
- "continuous": true,
64
- "dependsOn": [
65
- { "projects": ["trpc-node"], "target": "serve" }
66
- ]
67
- },
68
- "extract-i18n": {
69
- "executor": "@angular-devkit/build-angular:extract-i18n",
70
- "options": {
71
- "buildTarget": "examples-angular:build"
72
- }
73
- },
74
- "lint": {
75
- "executor": "@nx/eslint:lint"
76
- },
77
- "serve-static": {
78
- "executor": "@nx/web:file-server",
79
- "options": {
80
- "buildTarget": "examples-angular:build",
81
- "staticFilePath": "dist/examples/angular/browser",
82
- "spa": true
83
- }
84
- }
85
- }
86
- }
Binary file
@@ -1,6 +0,0 @@
1
- <header>
2
- <input (keydown)="this.service.term.set(inputTerm.value)" #inputTerm/>
3
- </header>
4
- <main>
5
- <router-outlet></router-outlet>
6
- </main>
@@ -1,22 +0,0 @@
1
- :host {
2
- display: block;
3
- }
4
-
5
- header {
6
- padding: 0.5rem;
7
-
8
- input {
9
- padding-inline: 1rem;
10
- color: rgb(205, 214, 244);
11
- background: rgb(88, 91, 112);
12
- width: 100%;
13
- height: 3rem;
14
- border-radius: 0.5rem;
15
- }
16
- }
17
-
18
- main {
19
- display: block;
20
- width: 100%;
21
- padding-inline: 0.5rem;
22
- }
@@ -1,14 +0,0 @@
1
- import { Component, inject } from '@angular/core';
2
- import { RouterModule } from '@angular/router';
3
- import { SearchService } from './services/search.service';
4
- import { TRPC } from './services/trpc.client';
5
-
6
- @Component({
7
- imports: [RouterModule],
8
- selector: 'app-root',
9
- templateUrl: './app.component.html',
10
- styleUrl: './app.component.scss',
11
- })
12
- export class AppComponent {
13
- protected service = inject(SearchService);
14
- }
@@ -1,16 +0,0 @@
1
- import { ApplicationConfig, provideExperimentalZonelessChangeDetection } from '@angular/core';
2
- import { NoPreloading, provideRouter, withPreloading, withRouterConfig } from '@angular/router';
3
- import { appRoutes } from './app.routes';
4
-
5
- export const appConfig: ApplicationConfig = {
6
- providers: [
7
- provideExperimentalZonelessChangeDetection(),
8
- provideRouter(
9
- appRoutes,
10
- withPreloading(NoPreloading),
11
- withRouterConfig({
12
- onSameUrlNavigation: 'ignore'
13
- })
14
- )
15
- ],
16
- };
@@ -1,25 +0,0 @@
1
- import { Route } from '@angular/router';
2
-
3
- export const appRoutes: Route[] = [
4
- {
5
- path: 'search',
6
- loadComponent: () => import('./search/search.component').then(x => x.SearchComponent)
7
- },
8
- {
9
- path: 'product/:slug',
10
- loadComponent: () => import('./product/product.component').then(x => x.ProductComponent)
11
- },
12
- {
13
- path: 'identity',
14
- loadComponent: () => import('./identity/identity.component').then(x => x.IdentityComponent)
15
- },
16
- {
17
- path: 'cart',
18
- loadComponent: () => import('./cart/cart.component').then(x => x.CartComponent)
19
- },
20
- {
21
- path: '**',
22
- pathMatch: 'prefix',
23
- redirectTo: 'search'
24
- }
25
- ];
@@ -1,4 +0,0 @@
1
- <pre>{{ cart() | json }}</pre>
2
- <button (click)="add()">Add</button>
3
- <button (click)="adjust()">Adjust</button>
4
- <button (click)="remove()">Remove</button>
@@ -1,14 +0,0 @@
1
- :host {
2
- display: grid;
3
- grid-template-columns: 600px;
4
- justify-content: center;
5
- gap: 0.5rem;
6
- }
7
-
8
- pre {
9
- color: white;
10
- }
11
-
12
- input, button {
13
- height: 3rem;
14
- }
@@ -1,73 +0,0 @@
1
- import {
2
- ChangeDetectionStrategy,
3
- Component,
4
- inject,
5
- signal,
6
- ViewEncapsulation,
7
- } from '@angular/core';
8
- import { CommonModule } from '@angular/common';
9
- import { TRPC } from '../services/trpc.client';
10
- import { Cart } from '@reactionary/core';
11
-
12
- @Component({
13
- selector: 'app-cart',
14
- imports: [CommonModule],
15
- templateUrl: './cart.component.html',
16
- styleUrl: './cart.component.scss',
17
- encapsulation: ViewEncapsulation.ShadowDom,
18
- changeDetection: ChangeDetectionStrategy.OnPush,
19
- })
20
- export class CartComponent {
21
- protected trpc = inject(TRPC);
22
- protected cart = signal<Cart | undefined>(undefined);
23
-
24
- protected async add() {
25
- const c = await this.trpc.client.cartMutation.mutate([
26
- {
27
- mutation: 'add',
28
- cart: {
29
- key: this.cart()?.identifier.key || '',
30
- },
31
- product: {
32
- key: 'ad153f54-6ae9-4800-8e5e-b40a07eb87b4',
33
- },
34
- quantity: 2,
35
- },
36
- ]);
37
-
38
- this.cart.set(c);
39
- }
40
-
41
- protected async adjust() {
42
- const existing = this.cart();
43
-
44
- if (existing) {
45
- const c = await this.trpc.client.cartMutation.mutate([
46
- {
47
- mutation: 'adjustQuantity',
48
- cart: existing.identifier,
49
- item: existing.items[0].identifier,
50
- quantity: existing.items[0].quantity + 1,
51
- },
52
- ]);
53
-
54
- this.cart.set(c);
55
- }
56
- }
57
-
58
- protected async remove() {
59
- const existing = this.cart();
60
-
61
- if (existing) {
62
- const c = await this.trpc.client.cartMutation.mutate([
63
- {
64
- mutation: 'remove',
65
- cart: existing.identifier,
66
- item: existing.items[0].identifier,
67
- },
68
- ]);
69
-
70
- this.cart.set(c);
71
- }
72
- }
73
- }
@@ -1,6 +0,0 @@
1
- <pre>{{ identity() | json }}</pre>
2
- <input #username />
3
- <input #password />
4
- <button (click)="login(username.value, password.value)">Log in</button>
5
- <button (click)="logout()">Logout</button>
6
- <button (click)="refresh()">Refresh</button>
@@ -1,18 +0,0 @@
1
- :host {
2
- display: grid;
3
- grid-template-columns: 600px;
4
- justify-content: center;
5
- gap: 0.5rem;
6
- }
7
-
8
- pre {
9
- color: white;
10
- }
11
-
12
- input, button {
13
- height: 3rem;
14
- }
15
-
16
- h2 {
17
- color: white;
18
- }
@@ -1,49 +0,0 @@
1
- import {
2
- ChangeDetectionStrategy,
3
- Component,
4
- inject,
5
- signal,
6
- ViewEncapsulation,
7
- } from '@angular/core';
8
- import { CommonModule } from '@angular/common';
9
- import { TRPC } from '../services/trpc.client';
10
- import { Identity } from '@reactionary/core';
11
-
12
- @Component({
13
- selector: 'app-identity',
14
- imports: [CommonModule],
15
- templateUrl: './identity.component.html',
16
- styleUrl: './identity.component.scss',
17
- encapsulation: ViewEncapsulation.ShadowDom,
18
- changeDetection: ChangeDetectionStrategy.OnPush,
19
- })
20
- export class IdentityComponent {
21
- protected trpc = inject(TRPC);
22
- protected identity = signal<Identity | undefined>(undefined);
23
-
24
- protected async login(username: string, password: string) {
25
- const res = await this.trpc.client.identityMutation.mutate([
26
- {
27
- mutation: 'login',
28
- username,
29
- password
30
- }
31
- ]);
32
-
33
- this.identity.set(res);
34
- }
35
-
36
- protected async logout() {
37
- const res = await this.trpc.client.identityMutation.mutate([{
38
- mutation: 'logout'
39
- }])
40
-
41
- this.identity.set(res);
42
- }
43
-
44
- protected async refresh() {
45
- const res = await this.trpc.client.identity.query([{ query: 'self' }])
46
-
47
- this.identity.set(res[0]);
48
- }
49
- }
@@ -1,14 +0,0 @@
1
- @if (service.productResource.value(); as product) {
2
- <section>
3
- <img [src]="product.image" [alt]="product.name" />
4
- </section>
5
- <section>
6
- <h2>{{ product.name }}</h2>
7
- <h3>{{ product.description }}</h3>
8
- <ol>
9
- @for (attribute of product.attributes; track $index) {
10
- ${{ attribute | json }}
11
- }
12
- </ol>
13
- </section>
14
- }
@@ -1,11 +0,0 @@
1
- :host {
2
- display: grid;
3
- grid-template-columns: 1fr 1fr;
4
- gap: 1rem;
5
- color: rgb(205, 214, 244);
6
- }
7
-
8
- img {
9
- width: 100%;
10
- border-radius: 0.5rem;
11
- }
@@ -1,42 +0,0 @@
1
- import { Component, effect, inject } from '@angular/core';
2
- import { CommonModule } from '@angular/common';
3
- import { ProductService } from '../services/product.service';
4
- import { TRPC } from '../services/trpc.client';
5
-
6
- @Component({
7
- selector: 'app-product',
8
- imports: [CommonModule],
9
- templateUrl: './product.component.html',
10
- styleUrl: './product.component.scss',
11
- })
12
- export class ProductComponent {
13
- protected service = inject(ProductService);
14
- protected trpc = inject(TRPC);
15
-
16
- constructor() {
17
- effect(async () => {
18
- const product = this.service.productResource.value();
19
-
20
- console.log('product: ', product);
21
-
22
- if (product && product.skus.length > 0) {
23
- const inventory = await this.trpc.client.inventory.query([{
24
- query: 'sku',
25
- sku: product.skus[0].identifier.key,
26
- }]);
27
- console.log('inventory: ', inventory);
28
-
29
- const prices = await this.trpc.client.price.query([
30
- { sku: product.skus[0].identifier, query: 'sku' },
31
- ]);
32
- console.log('price: ', prices);
33
-
34
- const pricesWithUnknownSku = await this.trpc.client.price.query([
35
- { sku: product.skus[0].identifier, query: 'sku' },
36
- { sku: { key: '123456' }, query: 'sku' },
37
- ]);
38
- console.log('pricesWithUnknownSku: ', pricesWithUnknownSku);
39
- }
40
- });
41
- }
42
- }
@@ -1,35 +0,0 @@
1
- <aside>
2
- @for (facet of service.search()?.facets; track $index) {
3
- <details [open]="true">
4
- <summary>{{ facet.name }}</summary>
5
-
6
- <div class="content">
7
- @for (value of facet.values; track $index) {
8
- <label>
9
- <span>{{ value.name }}</span>
10
- <span>{{ value.count }}</span>
11
- <input type="checkbox" (click)="toggleFacet(value.identifier)" [checked]="value.active"/>
12
- </label>
13
- }
14
- </div>
15
- </details>
16
- }
17
- </aside>
18
- <section>
19
- @for (product of service.search()?.products; track $index) {
20
- <a [routerLink]="['/', 'product', product.slug]">
21
- <article>
22
- <img [src]="product.image.replace('w_200', 'w_200,h_200,')" [alt]="product.name" />
23
- <h3>{{ product.name }}</h3>
24
- </article>
25
- </a>
26
- }
27
- </section>
28
- <footer>
29
- <button (click)="previousPage()" [disabled]="hasPrevious()">
30
- &lt;
31
- </button>
32
- <button (click)="nextPage()" [disabled]="hasNext()">
33
- &gt;
34
- </button>
35
- </footer>