From 67e91cd371b841a7ff1ce32fab94bb2a02ee559a Mon Sep 17 00:00:00 2001 From: Aqil-Ahmad Date: Tue, 25 Nov 2025 19:35:10 +0500 Subject: [PATCH 1/4] feat(product-list): implement product list grid plus table view --- apps/api-harmonization/src/app.module.ts | 2 + .../src/modules/page/page.model.ts | 2 + apps/frontend/package.json | 3 +- apps/frontend/src/blocks/renderBlocks.tsx | 3 + package-lock.json | 81 +++++ packages/blocks/product-list/.gitignore | 57 ++++ packages/blocks/product-list/.prettierrc.mjs | 25 ++ .../blocks/product-list/eslint.config.mjs | 18 ++ .../product-list/lint-staged.config.mjs | 4 + packages/blocks/product-list/package.json | 55 ++++ .../src/api-harmonization/index.ts | 8 + .../api-harmonization/product-list.client.ts | 4 + .../product-list.controller.ts | 22 ++ .../api-harmonization/product-list.mapper.ts | 50 +++ .../api-harmonization/product-list.model.ts | 57 ++++ .../api-harmonization/product-list.module.ts | 31 ++ .../api-harmonization/product-list.request.ts | 10 + .../api-harmonization/product-list.service.ts | 39 +++ .../src/frontend/ProductList.client.tsx | 206 +++++++++++++ .../src/frontend/ProductList.renderer.tsx | 28 ++ .../src/frontend/ProductList.server.tsx | 26 ++ .../src/frontend/ProductList.types.ts | 16 + .../blocks/product-list/src/frontend/index.ts | 5 + packages/blocks/product-list/src/sdk/index.ts | 28 ++ .../product-list/src/sdk/product-list.ts | 32 ++ .../blocks/product-list/tsconfig.api.json | 14 + .../product-list/tsconfig.frontend.json | 22 ++ packages/blocks/product-list/tsconfig.json | 18 ++ .../blocks/product-list/tsconfig.sdk.json | 19 ++ .../framework/src/modules/cms/cms.model.ts | 1 + .../cms/models/blocks/product-list.model.ts | 46 +++ .../mocked/src/modules/cms/cms.service.ts | 5 + .../mappers/blocks/cms.product-list.mapper.ts | 287 ++++++++++++++++++ .../modules/cms/mappers/cms.page.mapper.ts | 14 + .../mappers/mocks/pages/product-list.page.ts | 103 +++++++ 35 files changed, 1340 insertions(+), 1 deletion(-) create mode 100644 packages/blocks/product-list/.gitignore create mode 100644 packages/blocks/product-list/.prettierrc.mjs create mode 100644 packages/blocks/product-list/eslint.config.mjs create mode 100644 packages/blocks/product-list/lint-staged.config.mjs create mode 100644 packages/blocks/product-list/package.json create mode 100644 packages/blocks/product-list/src/api-harmonization/index.ts create mode 100644 packages/blocks/product-list/src/api-harmonization/product-list.client.ts create mode 100644 packages/blocks/product-list/src/api-harmonization/product-list.controller.ts create mode 100644 packages/blocks/product-list/src/api-harmonization/product-list.mapper.ts create mode 100644 packages/blocks/product-list/src/api-harmonization/product-list.model.ts create mode 100644 packages/blocks/product-list/src/api-harmonization/product-list.module.ts create mode 100644 packages/blocks/product-list/src/api-harmonization/product-list.request.ts create mode 100644 packages/blocks/product-list/src/api-harmonization/product-list.service.ts create mode 100644 packages/blocks/product-list/src/frontend/ProductList.client.tsx create mode 100644 packages/blocks/product-list/src/frontend/ProductList.renderer.tsx create mode 100644 packages/blocks/product-list/src/frontend/ProductList.server.tsx create mode 100644 packages/blocks/product-list/src/frontend/ProductList.types.ts create mode 100644 packages/blocks/product-list/src/frontend/index.ts create mode 100644 packages/blocks/product-list/src/sdk/index.ts create mode 100644 packages/blocks/product-list/src/sdk/product-list.ts create mode 100644 packages/blocks/product-list/tsconfig.api.json create mode 100644 packages/blocks/product-list/tsconfig.frontend.json create mode 100644 packages/blocks/product-list/tsconfig.json create mode 100644 packages/blocks/product-list/tsconfig.sdk.json create mode 100644 packages/framework/src/modules/cms/models/blocks/product-list.model.ts create mode 100644 packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.product-list.mapper.ts create mode 100644 packages/integrations/mocked/src/modules/cms/mappers/mocks/pages/product-list.page.ts diff --git a/apps/api-harmonization/src/app.module.ts b/apps/api-harmonization/src/app.module.ts index f9ef4d660..a9eaac0d2 100644 --- a/apps/api-harmonization/src/app.module.ts +++ b/apps/api-harmonization/src/app.module.ts @@ -39,6 +39,7 @@ import * as OrderList from '@o2s/blocks.order-list/api-harmonization'; import * as OrdersSummary from '@o2s/blocks.orders-summary/api-harmonization'; import * as PaymentsHistory from '@o2s/blocks.payments-history/api-harmonization'; import * as PaymentsSummary from '@o2s/blocks.payments-summary/api-harmonization'; +import * as ProductList from '@o2s/blocks.product-list/api-harmonization'; import * as QuickLinks from '@o2s/blocks.quick-links/api-harmonization'; import * as ServiceDetails from '@o2s/blocks.service-details/api-harmonization'; import * as ServiceList from '@o2s/blocks.service-list/api-harmonization'; @@ -133,6 +134,7 @@ export const AuthModuleBaseModule = AuthModule.Module.register(AppConfig); ArticleSearch.Module.register(AppConfig), FeaturedServiceList.Module.register(AppConfig), ArticleList.Module.register(AppConfig), + ProductList.Module.register(AppConfig), // BLOCK REGISTER ], providers: [ diff --git a/apps/api-harmonization/src/modules/page/page.model.ts b/apps/api-harmonization/src/modules/page/page.model.ts index 73fd1b1f8..52492c2fa 100644 --- a/apps/api-harmonization/src/modules/page/page.model.ts +++ b/apps/api-harmonization/src/modules/page/page.model.ts @@ -15,6 +15,7 @@ import * as OrderList from '@o2s/blocks.order-list/api-harmonization'; import * as OrdersSummary from '@o2s/blocks.orders-summary/api-harmonization'; import * as PaymentsHistory from '@o2s/blocks.payments-history/api-harmonization'; import * as PaymentsSummary from '@o2s/blocks.payments-summary/api-harmonization'; +import * as ProductList from '@o2s/blocks.product-list/api-harmonization'; import * as QuickLinks from '@o2s/blocks.quick-links/api-harmonization'; import * as ServiceDetails from '@o2s/blocks.service-details/api-harmonization'; import * as ServiceList from '@o2s/blocks.service-list/api-harmonization'; @@ -73,6 +74,7 @@ export class PageData { export type Blocks = // BLOCK REGISTER + | ProductList.Model.ProductListBlock['__typename'] | ArticleList.Model.ArticleListBlock['__typename'] | Category.Model.CategoryBlock['__typename'] | Article.Model.ArticleBlock['__typename'] diff --git a/apps/frontend/package.json b/apps/frontend/package.json index df599b98b..06954ee4a 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -6,7 +6,7 @@ "dev": "next dev --turbopack", "dev:https": "next dev --turbopack --experimental-https", "build": "next build", - "postbuild": "cp -r public .next/standalone/apps/frontend && cp -r .next/static .next/standalone/apps/frontend/.next/", + "postbuild": "shx cp -r public .next/standalone/apps/frontend && shx cp -r .next/static .next/standalone/apps/frontend/.next/", "start": "node .next/standalone/apps/frontend/server.js", "lint": "tsc --noEmit && next lint", "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,scss,json}\"", @@ -78,6 +78,7 @@ "postcss": "^8.5.6", "prettier": "^3.6.2", "sass": "^1.94.2", + "shx": "^0.3.4", "tailwindcss": "^4.1.17", "tsx": "^4.20.6", "typescript": "^5.9.3" diff --git a/apps/frontend/src/blocks/renderBlocks.tsx b/apps/frontend/src/blocks/renderBlocks.tsx index 0976f0ae6..40b8f62d0 100644 --- a/apps/frontend/src/blocks/renderBlocks.tsx +++ b/apps/frontend/src/blocks/renderBlocks.tsx @@ -16,6 +16,7 @@ import * as OrderList from '@o2s/blocks.order-list/frontend'; import * as OrdersSummary from '@o2s/blocks.orders-summary/frontend'; import * as PaymentsHistory from '@o2s/blocks.payments-history/frontend'; import * as PaymentsSummary from '@o2s/blocks.payments-summary/frontend'; +import * as ProductList from '@o2s/blocks.product-list/frontend'; import * as QuickLinks from '@o2s/blocks.quick-links/frontend'; import * as ServiceDetails from '@o2s/blocks.service-details/frontend'; import * as ServiceList from '@o2s/blocks.service-list/frontend'; @@ -129,6 +130,8 @@ const renderBlock = (typename: string, blockProps: BlockProps) => { return ; case 'FeaturedServiceListBlock': return ; + case 'ProductListBlock': + return ; // BLOCK REGISTER default: return null; diff --git a/package-lock.json b/package-lock.json index 4e099ca16..b58a749f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12786,6 +12786,10 @@ "resolved": "packages/blocks/payments-summary", "link": true }, + "node_modules/@o2s/blocks.product-list": { + "resolved": "packages/blocks/product-list", + "link": true + }, "node_modules/@o2s/blocks.quick-links": { "resolved": "packages/blocks/quick-links", "link": true @@ -47627,6 +47631,83 @@ "tailwindcss": "^4" } }, + "packages/blocks/product-list": { + "name": "@o2s/blocks.product-list", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "@o2s/configs.integrations": "*", + "@o2s/framework": "*", + "@o2s/ui": "*", + "@o2s/utils.api-harmonization": "*", + "@o2s/utils.frontend": "*", + "@o2s/utils.logger": "*" + }, + "devDependencies": { + "@o2s/eslint-config": "*", + "@o2s/prettier-config": "*", + "@o2s/typescript-config": "*", + "concurrently": "^9.1.2", + "dotenv-cli": "^8.0.0", + "eslint": "^9.27.0", + "prettier": "^3.5.3", + "tsc-alias": "^1.8.16", + "typescript": "^5.8.3" + }, + "peerDependencies": { + "@nestjs/axios": "^4", + "@nestjs/common": "^11", + "@nestjs/config": "^4.0.2", + "@nestjs/core": "^11", + "@types/react": "^19", + "@types/react-dom": "^19", + "next": "^15.3.2", + "next-intl": "^4.1.0", + "react": "^19", + "react-dom": "^19", + "rxjs": "^7", + "tailwindcss": "^4" + } + }, + "packages/blocks/product-list/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "packages/blocks/product-list/node_modules/dotenv-cli": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-8.0.0.tgz", + "integrity": "sha512-aLqYbK7xKOiTMIRf1lDPbI+Y+Ip/wo5k3eyp6ePysVaSqbyxjyK3dK35BTxG+rmd7djf5q2UPs4noPNH+cj0Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.6", + "dotenv": "^16.3.0", + "dotenv-expand": "^10.0.0", + "minimist": "^1.2.6" + }, + "bin": { + "dotenv": "cli.js" + } + }, + "packages/blocks/product-list/node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, "packages/blocks/quick-links": { "name": "@o2s/blocks.quick-links", "version": "1.1.0", diff --git a/packages/blocks/product-list/.gitignore b/packages/blocks/product-list/.gitignore new file mode 100644 index 000000000..29986a380 --- /dev/null +++ b/packages/blocks/product-list/.gitignore @@ -0,0 +1,57 @@ +# compiled output +/dist +/node_modules +/build + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local + +# temp directory +.temp +.tmp + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +/tsconfig.tsbuildinfo diff --git a/packages/blocks/product-list/.prettierrc.mjs b/packages/blocks/product-list/.prettierrc.mjs new file mode 100644 index 000000000..93b66d398 --- /dev/null +++ b/packages/blocks/product-list/.prettierrc.mjs @@ -0,0 +1,25 @@ +import apiConfig from "@o2s/prettier-config/api.mjs"; +import frontendConfig from "@o2s/prettier-config/frontend.mjs"; + +/** + * @see https://prettier.io/docs/en/configuration.html + * @type {import("prettier").Config} + */ +const config = { + overrides: [ + { + files: "./src/api-harmonization/**/*", + options: apiConfig, + }, + { + files: "./src/frontend/**/*", + options: frontendConfig, + }, + { + files: "./src/sdk/**/*", + options: frontendConfig, + }, + ], +}; + +export default config; diff --git a/packages/blocks/product-list/eslint.config.mjs b/packages/blocks/product-list/eslint.config.mjs new file mode 100644 index 000000000..223f2af08 --- /dev/null +++ b/packages/blocks/product-list/eslint.config.mjs @@ -0,0 +1,18 @@ +import { config as apiConfig } from '@o2s/eslint-config/api'; +import { config as frontendConfig } from '@o2s/eslint-config/frontend-block'; +import { defineConfig } from 'eslint/config'; + +export default defineConfig([ + { + files: ['src/api-harmonization/**/*'], + extends: [apiConfig], + }, + { + files: ['src/frontend/**/*'], + extends: [frontendConfig], + }, + { + files: ['src/sdk/**/*'], + extends: [frontendConfig], + }, +]); diff --git a/packages/blocks/product-list/lint-staged.config.mjs b/packages/blocks/product-list/lint-staged.config.mjs new file mode 100644 index 000000000..ff4483ccb --- /dev/null +++ b/packages/blocks/product-list/lint-staged.config.mjs @@ -0,0 +1,4 @@ +export default { + '*.{js,jsx,ts,tsx,css,scss}': ['prettier --write'], + '*.{js,jsx,ts,tsx}': () => 'tsc --noEmit', +}; diff --git a/packages/blocks/product-list/package.json b/packages/blocks/product-list/package.json new file mode 100644 index 000000000..d1946909e --- /dev/null +++ b/packages/blocks/product-list/package.json @@ -0,0 +1,55 @@ +{ + "name": "@o2s/blocks.product-list", + "version": "0.0.1", + "private": false, + "license": "MIT", + "description": "A block for displaying and filtering a list of products with grid and table views.", + "exports": { + "./api-harmonization": "./dist/api-harmonization/api-harmonization/index.js", + "./frontend": "./dist/frontend/frontend/index.js", + "./sdk": "./dist/sdk/sdk/index.js", + "./client": "./dist/api-harmonization/api-harmonization/product-list.client.js" + }, + "files": [ + "dist" + ], + "scripts": { + "dev": "tsc --build tsconfig.json && (concurrently \"tsc --build tsconfig.json -w\" \"tsc-alias -w\")", + "build": "tsc --build tsconfig.json && tsc-alias", + "lint": "tsc --noEmit && eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,scss,json}\"" + }, + "dependencies": { + "@o2s/framework": "*", + "@o2s/utils.logger": "*", + "@o2s/ui": "*", + "@o2s/utils.api-harmonization": "*", + "@o2s/utils.frontend": "*", + "@o2s/configs.integrations": "*" + }, + "devDependencies": { + "dotenv-cli": "^8.0.0", + "@o2s/eslint-config": "*", + "@o2s/prettier-config": "*", + "@o2s/typescript-config": "*", + "concurrently": "^9.1.2", + "eslint": "^9.27.0", + "prettier": "^3.5.3", + "tsc-alias": "^1.8.16", + "typescript": "^5.8.3" + }, + "peerDependencies": { + "@types/react": "^19", + "@types/react-dom": "^19", + "react": "^19", + "react-dom": "^19", + "tailwindcss": "^4", + "@nestjs/axios": "^4", + "@nestjs/common": "^11", + "@nestjs/config": "^4.0.2", + "@nestjs/core": "^11", + "rxjs": "^7", + "next": "^15.3.2", + "next-intl": "^4.1.0" + } +} diff --git a/packages/blocks/product-list/src/api-harmonization/index.ts b/packages/blocks/product-list/src/api-harmonization/index.ts new file mode 100644 index 000000000..43e1ee0d0 --- /dev/null +++ b/packages/blocks/product-list/src/api-harmonization/index.ts @@ -0,0 +1,8 @@ +export const URL = '/blocks/product-list'; + +export { ProductListBlockModule as Module } from './product-list.module'; +export { ProductListService as Service } from './product-list.service'; +export { ProductListController as Controller } from './product-list.controller'; + +export * as Model from './product-list.model'; +export * as Request from './product-list.request'; diff --git a/packages/blocks/product-list/src/api-harmonization/product-list.client.ts b/packages/blocks/product-list/src/api-harmonization/product-list.client.ts new file mode 100644 index 000000000..b9ba3aa30 --- /dev/null +++ b/packages/blocks/product-list/src/api-harmonization/product-list.client.ts @@ -0,0 +1,4 @@ +export const URL = '/blocks/product-list'; + +export * as Model from './product-list.model'; +export * as Request from './product-list.request'; diff --git a/packages/blocks/product-list/src/api-harmonization/product-list.controller.ts b/packages/blocks/product-list/src/api-harmonization/product-list.controller.ts new file mode 100644 index 000000000..6a2bd3bc4 --- /dev/null +++ b/packages/blocks/product-list/src/api-harmonization/product-list.controller.ts @@ -0,0 +1,22 @@ +import { Controller, Get, Headers, Query, UseInterceptors } from '@nestjs/common'; + +import { Models } from '@o2s/utils.api-harmonization'; +import { LoggerService } from '@o2s/utils.logger'; + +import { Auth } from '@o2s/framework/modules'; + +import { URL } from './'; +import { GetProductListBlockQuery } from './product-list.request'; +import { ProductListService } from './product-list.service'; + +@Controller(URL) +@UseInterceptors(LoggerService) +export class ProductListController { + constructor(protected readonly service: ProductListService) {} + + @Get() + @Auth.Decorators.Roles({ roles: [] }) + getProductListBlock(@Headers() headers: Models.Headers.AppHeaders, @Query() query: GetProductListBlockQuery) { + return this.service.getProductListBlock(query, headers); + } +} diff --git a/packages/blocks/product-list/src/api-harmonization/product-list.mapper.ts b/packages/blocks/product-list/src/api-harmonization/product-list.mapper.ts new file mode 100644 index 000000000..b95b35d21 --- /dev/null +++ b/packages/blocks/product-list/src/api-harmonization/product-list.mapper.ts @@ -0,0 +1,50 @@ +import { CMS, Products } from '@o2s/configs.integrations'; + +import { ProductItem, ProductListBlock } from './product-list.model'; + +export const mapProductList = ( + products: Products.Model.Products, + cms: CMS.Model.ProductListBlock.ProductListBlock, + _locale: string, +): ProductListBlock => { + return { + __typename: 'ProductListBlock', + id: cms.id, + title: cms.title, + subtitle: cms.subtitle, + detailsLabel: cms.detailsLabel, + pagination: cms.pagination, + filters: cms.filters as unknown as ProductListBlock['filters'], + table: cms.table as unknown as ProductListBlock['table'], + noResults: cms.noResults, + labels: cms.labels, + products: { + total: products.total, + data: products.data.map((product) => mapProduct(product)), + }, + }; +}; + +const mapProduct = (product: Products.Model.Product): ProductItem => { + return { + __typename: 'ProductItem', + id: product.id, + sku: product.sku, + name: product.name, + description: product.description, + shortDescription: product.shortDescription, + detailsUrl: product.link, + type: { + value: product.type, + label: product.type, + }, + category: { + value: product.category, + label: product.category, + }, + price: product.price, + image: product.image, + tags: product.tags, + link: product.link, + }; +}; diff --git a/packages/blocks/product-list/src/api-harmonization/product-list.model.ts b/packages/blocks/product-list/src/api-harmonization/product-list.model.ts new file mode 100644 index 000000000..fac9f017f --- /dev/null +++ b/packages/blocks/product-list/src/api-harmonization/product-list.model.ts @@ -0,0 +1,57 @@ +import { Products } from '@o2s/configs.integrations'; + +import { Models as ApiModels } from '@o2s/utils.api-harmonization'; + +import { Models } from '@o2s/framework/modules'; + +export class ProductListBlock extends ApiModels.Block.Block { + __typename!: 'ProductListBlock'; + title!: string; + subtitle?: string; + detailsLabel?: string; + filters?: Models.Filters.Filters; + pagination?: Models.Pagination.Pagination; + products!: { + data: ProductItem[]; + total: number; + }; + table!: Models.DataTable.DataTable; + noResults!: { + title: string; + description?: string; + }; + labels!: { + clickToSelect: string; + gridView: string; + tableView: string; + }; +} + +export type ProductList = { + data: ProductItem[]; + total: number; +}; + +export class ProductItem { + __typename!: 'ProductItem'; + id!: string; + sku!: string; + name!: string; + description!: string; + shortDescription?: string; + detailsUrl!: string; + type!: { + value: Products.Model.ProductType; + label: string; + }; + category!: { + value: string; + label: string; + }; + price!: Models.Price.Price; + image?: Models.Media.Media; + tags!: Products.Model.Product['tags']; + link!: string; +} + +export type Product = ProductItem; diff --git a/packages/blocks/product-list/src/api-harmonization/product-list.module.ts b/packages/blocks/product-list/src/api-harmonization/product-list.module.ts new file mode 100644 index 000000000..10f376f46 --- /dev/null +++ b/packages/blocks/product-list/src/api-harmonization/product-list.module.ts @@ -0,0 +1,31 @@ +import { HttpModule } from '@nestjs/axios'; +import { DynamicModule, Module } from '@nestjs/common'; +import { CMS, Products } from '@o2s/configs.integrations'; + +import * as Framework from '@o2s/framework/modules'; + +import { ProductListController } from './product-list.controller'; +import { ProductListService } from './product-list.service'; + +@Module({}) +export class ProductListBlockModule { + static register(_config: Framework.ApiConfig): DynamicModule { + return { + module: ProductListBlockModule, + providers: [ + ProductListService, + { + provide: CMS.Service, + useExisting: Framework.CMS.Service, + }, + { + provide: Products.Service, + useExisting: Framework.Products.Service, + }, + ], + controllers: [ProductListController], + imports: [HttpModule], + exports: [ProductListService], + }; + } +} diff --git a/packages/blocks/product-list/src/api-harmonization/product-list.request.ts b/packages/blocks/product-list/src/api-harmonization/product-list.request.ts new file mode 100644 index 000000000..65a63070a --- /dev/null +++ b/packages/blocks/product-list/src/api-harmonization/product-list.request.ts @@ -0,0 +1,10 @@ +import { CMS, Products } from '@o2s/framework/modules'; + +export class GetProductListBlockQuery implements Omit { + id!: string; + limit?: number; + offset?: number; + type?: Products.Model.ProductType; + category?: string; + sort?: string; +} diff --git a/packages/blocks/product-list/src/api-harmonization/product-list.service.ts b/packages/blocks/product-list/src/api-harmonization/product-list.service.ts new file mode 100644 index 000000000..4ec7d6bcd --- /dev/null +++ b/packages/blocks/product-list/src/api-harmonization/product-list.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@nestjs/common'; +import { CMS, Products } from '@o2s/configs.integrations'; +import { Observable, concatMap, forkJoin, map } from 'rxjs'; + +import { Models } from '@o2s/utils.api-harmonization'; + +import { mapProductList } from './product-list.mapper'; +import { ProductListBlock } from './product-list.model'; +import { GetProductListBlockQuery } from './product-list.request'; + +@Injectable() +export class ProductListService { + constructor( + private readonly cmsService: CMS.Service, + private readonly productsService: Products.Service, + ) {} + + getProductListBlock( + query: GetProductListBlockQuery, + headers: Models.Headers.AppHeaders, + ): Observable { + const cms = this.cmsService.getProductListBlock({ ...query, locale: headers['x-locale'] }); + + return forkJoin([cms]).pipe( + concatMap(([cms]) => { + return this.productsService + .getProductList({ + ...query, + limit: query.limit || cms.pagination?.limit || 12, + offset: query.offset || 0, + type: query.type as Products.Model.ProductType, + category: query.category, + locale: headers['x-locale'], + }) + .pipe(map((products) => mapProductList(products, cms, headers['x-locale']))); + }), + ); + } +} diff --git a/packages/blocks/product-list/src/frontend/ProductList.client.tsx b/packages/blocks/product-list/src/frontend/ProductList.client.tsx new file mode 100644 index 000000000..4d06d50e3 --- /dev/null +++ b/packages/blocks/product-list/src/frontend/ProductList.client.tsx @@ -0,0 +1,206 @@ +'use client'; + +import { ArrowRight, LayoutGrid, Table as TableIcon } from 'lucide-react'; +import { createNavigation } from 'next-intl/navigation'; +import React, { useState, useTransition } from 'react'; + +import { ProductCard, ProductCardBadge } from '@o2s/ui/components/Cards/ProductCard'; +import { DataList } from '@o2s/ui/components/DataList'; +import type { DataListColumnConfig } from '@o2s/ui/components/DataList'; +import { FiltersSection } from '@o2s/ui/components/Filters'; +import { NoResults } from '@o2s/ui/components/NoResults'; +import { Pagination } from '@o2s/ui/components/Pagination'; + +import { Button } from '@o2s/ui/elements/button'; +import { LoadingOverlay } from '@o2s/ui/elements/loading-overlay'; +import { Separator } from '@o2s/ui/elements/separator'; +import { ToggleGroup, ToggleGroupItem } from '@o2s/ui/elements/toggle-group'; + +import { Model } from '../api-harmonization/product-list.client'; +import { sdk } from '../sdk'; + +import { ProductListPureProps } from './ProductList.types'; + +type ViewMode = 'grid' | 'table'; + +export const ProductListPure: React.FC = ({ locale, accessToken, routing, ...component }) => { + const initialFilters = { + id: component.id, + offset: 0, + limit: component.pagination?.limit || 12, + }; + + const initialData = component.products.data; + const [data, setData] = useState(component); + const [filters, setFilters] = useState(initialFilters); + const [isPending, startTransition] = useTransition(); + const [viewMode, setViewMode] = useState('grid'); + + const handleFilter = (data: Partial) => { + startTransition(async () => { + const newFilters = { ...filters, ...data }; + const newData = await sdk.blocks.getProductList(newFilters, { 'x-locale': locale }, accessToken); + setFilters(newFilters); + setData(newData); + }); + }; + + const handleReset = () => { + startTransition(async () => { + const newData = await sdk.blocks.getProductList(initialFilters, { 'x-locale': locale }, accessToken); + setFilters(initialFilters); + setData(newData); + }); + }; + + const { Link: LinkComponent } = createNavigation(routing); + + // Define table columns configuration + const columns = data.table.columns.map((column) => { + switch (column.id) { + case 'name': + return { + ...column, + type: 'text', + cellClassName: 'max-w-[200px] lg:max-w-md font-medium', + }; + case 'price': + return { + ...column, + type: 'price', + }; + case 'type': + case 'category': + return { + ...column, + type: 'text', + }; + default: + return { + ...column, + type: 'text', + }; + } + }) as DataListColumnConfig[]; + + const actions = data.table.actions + ? { + ...data.table.actions, + render: (product: Model.Product) => ( + + ), + } + : undefined; + + return ( +
+ {initialData.length > 0 ? ( +
+
+ + + value && setViewMode(value as ViewMode)} + variant="solid" + className="flex-shrink-0" + > + + + {data.labels.gridView} + + + + {data.labels.tableView} + + +
+ + + {data.products.data.length ? ( +
+ {viewMode === 'grid' ? ( +
    + {data.products.data.map((product) => ( +
  • + +
  • + ))} +
+ ) : ( +
+ + data={data.products.data} + columns={columns} + actions={actions} + getRowKey={(item) => item.id} + /> +
+ )} + + {data.pagination && ( + { + handleFilter({ + ...filters, + offset: data.pagination!.limit * (page - 1), + }); + }} + /> + )} +
+ ) : ( +
+ + + +
+ )} +
+
+ ) : ( +
+ + + +
+ )} +
+ ); +}; diff --git a/packages/blocks/product-list/src/frontend/ProductList.renderer.tsx b/packages/blocks/product-list/src/frontend/ProductList.renderer.tsx new file mode 100644 index 000000000..a06fb8250 --- /dev/null +++ b/packages/blocks/product-list/src/frontend/ProductList.renderer.tsx @@ -0,0 +1,28 @@ +import { useLocale } from 'next-intl'; +import React, { Suspense } from 'react'; + +import { Container } from '@o2s/ui/components/Container'; +import { Loading } from '@o2s/ui/components/Loading'; + +import { ProductList } from './ProductList.server'; +import { ProductListRendererProps } from './ProductList.types'; + +export const ProductListRenderer: React.FC = ({ id, accessToken, routing }) => { + const locale = useLocale(); + + return ( + + + + + + + } + > + + + ); +}; diff --git a/packages/blocks/product-list/src/frontend/ProductList.server.tsx b/packages/blocks/product-list/src/frontend/ProductList.server.tsx new file mode 100644 index 000000000..c4577fc3f --- /dev/null +++ b/packages/blocks/product-list/src/frontend/ProductList.server.tsx @@ -0,0 +1,26 @@ +import dynamic from 'next/dynamic'; +import React from 'react'; + +import { sdk } from '../sdk'; + +import { ProductListProps } from './ProductList.types'; + +export const ProductListDynamic = dynamic(() => + import('./ProductList.client').then((module) => module.ProductListPure), +); + +export const ProductList: React.FC = async ({ id, accessToken, locale, routing }) => { + try { + const data = await sdk.blocks.getProductList( + { + id, + }, + { 'x-locale': locale }, + accessToken, + ); + + return ; + } catch (_error) { + return null; + } +}; diff --git a/packages/blocks/product-list/src/frontend/ProductList.types.ts b/packages/blocks/product-list/src/frontend/ProductList.types.ts new file mode 100644 index 000000000..41b6cbfc0 --- /dev/null +++ b/packages/blocks/product-list/src/frontend/ProductList.types.ts @@ -0,0 +1,16 @@ +import { defineRouting } from 'next-intl/routing'; + +import { Model } from '../api-harmonization/product-list.client'; + +export interface ProductListProps { + id: string; + accessToken?: string; + locale: string; + routing: ReturnType; +} + +export type ProductListPureProps = ProductListProps & Model.ProductListBlock; + +export type ProductListRendererProps = Omit & { + slug: string[]; +}; diff --git a/packages/blocks/product-list/src/frontend/index.ts b/packages/blocks/product-list/src/frontend/index.ts new file mode 100644 index 000000000..83be12f6a --- /dev/null +++ b/packages/blocks/product-list/src/frontend/index.ts @@ -0,0 +1,5 @@ +export { ProductListPure as Client } from './ProductList.client'; +export { ProductList as Server } from './ProductList.server'; +export { ProductListRenderer as Renderer } from './ProductList.renderer'; + +export * as Types from './ProductList.types'; diff --git a/packages/blocks/product-list/src/sdk/index.ts b/packages/blocks/product-list/src/sdk/index.ts new file mode 100644 index 000000000..b7eff2d4a --- /dev/null +++ b/packages/blocks/product-list/src/sdk/index.ts @@ -0,0 +1,28 @@ +// this unused import is necessary for TypeScript to properly resolve API methods +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { Models } from '@o2s/utils.api-harmonization'; + +import { extendSdk, getSdk } from '@o2s/framework/sdk'; + +import { productList } from './product-list'; + +const API_URL = + (typeof window === 'undefined' ? process.env.NEXT_PUBLIC_API_URL_INTERNAL : process.env.NEXT_PUBLIC_API_URL) || + process.env.NEXT_PUBLIC_API_URL; + +const internalSdk = getSdk({ + apiUrl: API_URL!, + logger: { + // @ts-expect-error missing types + level: process.env.NEXT_PUBLIC_LOG_LEVEL, + // @ts-expect-error missing types + format: process.env.NEXT_PUBLIC_LOG_FORMAT, + colorsEnabled: process.env.NEXT_PUBLIC_LOG_COLORS_ENABLED === 'true', + }, +}); + +export const sdk = extendSdk(internalSdk, { + blocks: { + getProductList: productList(internalSdk).blocks.getProductList, + }, +}); diff --git a/packages/blocks/product-list/src/sdk/product-list.ts b/packages/blocks/product-list/src/sdk/product-list.ts new file mode 100644 index 000000000..9168d878e --- /dev/null +++ b/packages/blocks/product-list/src/sdk/product-list.ts @@ -0,0 +1,32 @@ +import { Models } from '@o2s/utils.api-harmonization'; +import { Utils } from '@o2s/utils.frontend'; + +import { Sdk } from '@o2s/framework/sdk'; + +import { Model, Request, URL } from '../api-harmonization/product-list.client'; + +const API_URL = URL; + +export const productList = (sdk: Sdk) => ({ + blocks: { + getProductList: ( + query: Request.GetProductListBlockQuery, + headers: Models.Headers.AppHeaders, + authorization?: string, + ): Promise => + sdk.makeRequest({ + method: 'get', + url: `${API_URL}`, + headers: { + ...Utils.Headers.getApiHeaders(), + ...headers, + ...(authorization + ? { + Authorization: `Bearer ${authorization}`, + } + : {}), + }, + params: query, + }), + }, +}); diff --git a/packages/blocks/product-list/tsconfig.api.json b/packages/blocks/product-list/tsconfig.api.json new file mode 100644 index 000000000..0f9f79f8e --- /dev/null +++ b/packages/blocks/product-list/tsconfig.api.json @@ -0,0 +1,14 @@ +{ + "extends": "@o2s/typescript-config/api.json", + "compilerOptions": { + "outDir": "./dist/api-harmonization", + "rootDir": "./src", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "composite": true, + "declaration": true, + "declarationMap": true, + "baseUrl": "./src/api-harmonization", + }, + "include": ["src/api-harmonization"] +} diff --git a/packages/blocks/product-list/tsconfig.frontend.json b/packages/blocks/product-list/tsconfig.frontend.json new file mode 100644 index 000000000..6820b8689 --- /dev/null +++ b/packages/blocks/product-list/tsconfig.frontend.json @@ -0,0 +1,22 @@ +{ + "extends": "@o2s/typescript-config/frontend.json", + "compilerOptions": { + "outDir": "./dist/frontend", + "rootDir": "./src", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "composite": true, + "declaration": true, + "declarationMap": true, + "noEmit": false, + "jsx": "react", + "baseUrl": "./src/frontend" + }, + "include": [ + "src/frontend", + "src/api-harmonization/product-list.client.ts", + "src/api-harmonization/product-list.model.ts", + "src/api-harmonization/product-list.request.ts", + "src/sdk" + ] +} diff --git a/packages/blocks/product-list/tsconfig.json b/packages/blocks/product-list/tsconfig.json new file mode 100644 index 000000000..c3031c1dd --- /dev/null +++ b/packages/blocks/product-list/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "@o2s/typescript-config/base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "composite": true, + "declaration": true, + "declarationMap": true, + "baseUrl": "./src", + }, + "references": [ + { "path": "./tsconfig.frontend.json" }, + { "path": "./tsconfig.api.json" }, + { "path": "./tsconfig.sdk.json" } + ] +} diff --git a/packages/blocks/product-list/tsconfig.sdk.json b/packages/blocks/product-list/tsconfig.sdk.json new file mode 100644 index 000000000..2e3894a6b --- /dev/null +++ b/packages/blocks/product-list/tsconfig.sdk.json @@ -0,0 +1,19 @@ +{ + "extends": "@o2s/typescript-config/api.json", + "compilerOptions": { + "outDir": "./dist/sdk", + "rootDir": "./src", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "composite": true, + "declaration": true, + "declarationMap": true, + "baseUrl": "./src/sdk" + }, + "include": [ + "src/sdk", + "src/api-harmonization/product-list.client.ts", + "src/api-harmonization/product-list.model.ts", + "src/api-harmonization/product-list.request.ts" + ] +} diff --git a/packages/framework/src/modules/cms/cms.model.ts b/packages/framework/src/modules/cms/cms.model.ts index 8138efa0f..c2e5c2f8f 100644 --- a/packages/framework/src/modules/cms/cms.model.ts +++ b/packages/framework/src/modules/cms/cms.model.ts @@ -33,3 +33,4 @@ export * as CategoryListBlock from './models/blocks/category-list.model'; export * as QuickLinksBlock from './models/blocks/quick-links.model'; export * as ArticleSearchBlock from './models/blocks/article-search.model'; export * as FeaturedServiceListBlock from './models/blocks/featured-service-list.model'; +export * as ProductListBlock from './models/blocks/product-list.model'; diff --git a/packages/framework/src/modules/cms/models/blocks/product-list.model.ts b/packages/framework/src/modules/cms/models/blocks/product-list.model.ts new file mode 100644 index 000000000..cbd513646 --- /dev/null +++ b/packages/framework/src/modules/cms/models/blocks/product-list.model.ts @@ -0,0 +1,46 @@ +import { Product, Products } from '@/modules/products/products.model'; + +import { Block, DataTable, Filters, Pagination } from '@/utils/models'; + +type ProductKeys = keyof Product | string | '__typename'; + +type ProductTableColumn = Omit, 'id'> & { + id: ProductKeys; +}; + +type ProductDataTable = Omit, 'columns'> & { + columns: ProductTableColumn[]; +}; + +type ProductFilterSelect = Omit, 'id'> & { + id: ProductKeys; +}; + +type ProductFilterDateRange = Omit, 'id'> & { + id: ProductKeys; +}; + +type ProductFilterItem = ProductFilterSelect | ProductFilterDateRange; + +type ProductFilters = Omit, 'items'> & { + items: ProductFilterItem[]; +}; + +export class ProductListBlock extends Block.Block { + title!: string; + subtitle?: string; + products!: Products; + table!: ProductDataTable; + pagination?: Pagination.Pagination; + filters?: ProductFilters; + noResults!: { + title: string; + description: string; + }; + labels!: { + clickToSelect: string; + gridView: string; + tableView: string; + }; + detailsLabel?: string; +} diff --git a/packages/integrations/mocked/src/modules/cms/cms.service.ts b/packages/integrations/mocked/src/modules/cms/cms.service.ts index 9815be25c..4ccf16484 100644 --- a/packages/integrations/mocked/src/modules/cms/cms.service.ts +++ b/packages/integrations/mocked/src/modules/cms/cms.service.ts @@ -16,6 +16,7 @@ import { mapOrderListBlock } from './mappers/blocks/cms.order-list.mapper'; import { mapOrdersSummaryBlock } from './mappers/blocks/cms.orders-summary.mapper'; import { mapPaymentsHistoryBlock } from './mappers/blocks/cms.payments-history.mapper'; import { mapPaymentsSummaryBlock } from './mappers/blocks/cms.payments-summary.mapper'; +import { mapProductListBlock } from './mappers/blocks/cms.product-list.mapper'; import { mapResourceDetailsBlock } from './mappers/blocks/cms.resource-details.mapper'; import { mapResourceListBlock } from './mappers/blocks/cms.resource-list.mapper'; import { mapServiceDetailsBlock } from './mappers/blocks/cms.service-details.mapper'; @@ -187,4 +188,8 @@ export class CmsService implements CMS.Service { getFeaturedServiceListBlock(options: CMS.Request.GetCmsEntryParams) { return of(mapFeaturedServiceListBlock(options.locale)).pipe(responseDelay()); } + + getProductListBlock(options: CMS.Request.GetCmsEntryParams) { + return of(mapProductListBlock(options.locale)).pipe(responseDelay()); + } } diff --git a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.product-list.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.product-list.mapper.ts new file mode 100644 index 000000000..a26f87fc0 --- /dev/null +++ b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.product-list.mapper.ts @@ -0,0 +1,287 @@ +import { CMS } from '@o2s/framework/modules'; + +const MOCK_PRODUCT_LIST_BLOCK_EN: CMS.Model.ProductListBlock.ProductListBlock = { + id: 'product-list-1', + title: 'Products', + subtitle: 'Browse our product catalog', + detailsLabel: 'View Details', + products: { + data: [], + total: 0, + }, + table: { + columns: [ + { id: 'sku', title: 'SKU' }, + { id: 'name', title: 'Product Name' }, + { id: 'category', title: 'Category' }, + { id: 'type', title: 'Type' }, + { id: 'price', title: 'Price' }, + ], + actions: { + title: 'Actions', + label: 'View Details', + }, + }, + pagination: { + limit: 12, + legend: 'of {totalPages} pages', + prev: 'Previous', + next: 'Next', + selectPage: 'Select page', + }, + filters: { + label: 'Filter', + title: 'Filter Products', + description: 'Use filters to find specific products', + submit: 'Apply Filters', + reset: 'Reset Filters', + removeFilters: 'Remove filters ({active})', + close: 'Close filters', + items: [ + { + __typename: 'FilterSelect', + id: 'sort', + label: 'Sort by', + allowMultiple: false, + options: [ + { label: 'Name ascending', value: 'name_ASC' }, + { label: 'Name descending', value: 'name_DESC' }, + { label: 'Price ascending', value: 'price_ASC' }, + { label: 'Price descending', value: 'price_DESC' }, + ], + }, + { + __typename: 'FilterSelect', + id: 'type', + label: 'Product Type', + allowMultiple: true, + options: [ + { label: 'Physical', value: 'PHYSICAL' }, + { label: 'Virtual', value: 'VIRTUAL' }, + ], + }, + { + __typename: 'FilterSelect', + id: 'category', + label: 'Category', + allowMultiple: true, + options: [ + { label: 'Software', value: 'SOFTWARE' }, + { label: 'Tools', value: 'TOOLS' }, + { label: 'Hardware', value: 'HARDWARE' }, + { label: 'Measurement', value: 'MEASUREMENT' }, + { label: 'Cloud', value: 'CLOUD' }, + { label: 'Support', value: 'SUPPORT' }, + { label: 'Subscription', value: 'SUBSCRIPTION' }, + { label: 'Warranty', value: 'WARRANTY' }, + { label: 'Maintenance', value: 'MAINTENANCE' }, + { label: 'Training', value: 'TRAINING' }, + ], + }, + ], + }, + noResults: { + title: 'No Products Found', + description: 'There are no products matching your criteria', + }, + labels: { + clickToSelect: 'Click to select', + gridView: 'Grid view', + tableView: 'Table view', + }, +}; + +const MOCK_PRODUCT_LIST_BLOCK_DE: CMS.Model.ProductListBlock.ProductListBlock = { + id: 'product-list-1', + title: 'Produkte', + subtitle: 'Durchsuchen Sie unseren Produktkatalog', + detailsLabel: 'Details anzeigen', + products: { + data: [], + total: 0, + }, + table: { + columns: [ + { id: 'sku', title: 'SKU' }, + { id: 'name', title: 'Produktname' }, + { id: 'category', title: 'Kategorie' }, + { id: 'type', title: 'Typ' }, + { id: 'price', title: 'Preis' }, + ], + actions: { + title: 'Aktionen', + label: 'Details anzeigen', + }, + }, + pagination: { + limit: 12, + legend: 'von {totalPages} Seiten', + prev: 'Vorherige', + next: 'Nächste', + selectPage: 'Seite auswählen', + }, + filters: { + label: 'Filter', + title: 'Filter Produkte', + description: 'Verwenden Sie Filter, um spezifische Produkte zu finden', + submit: 'Filter anwenden', + reset: 'Filter zurücksetzen', + removeFilters: 'Filter entfernen ({active})', + close: 'Filter schließen', + items: [ + { + __typename: 'FilterSelect', + id: 'sort', + label: 'Sortieren nach', + allowMultiple: false, + options: [ + { label: 'Name aufsteigend', value: 'name_ASC' }, + { label: 'Name absteigend', value: 'name_DESC' }, + { label: 'Preis aufsteigend', value: 'price_ASC' }, + { label: 'Preis absteigend', value: 'price_DESC' }, + ], + }, + { + __typename: 'FilterSelect', + id: 'type', + label: 'Produkttyp', + allowMultiple: true, + options: [ + { label: 'Physikalisch', value: 'PHYSICAL' }, + { label: 'Virtuell', value: 'VIRTUAL' }, + ], + }, + { + __typename: 'FilterSelect', + id: 'category', + label: 'Kategorie', + allowMultiple: true, + options: [ + { label: 'Software', value: 'SOFTWARE' }, + { label: 'Werkzeuge', value: 'TOOLS' }, + { label: 'Hardware', value: 'HARDWARE' }, + { label: 'Messung', value: 'MEASUREMENT' }, + { label: 'Cloud', value: 'CLOUD' }, + { label: 'Support', value: 'SUPPORT' }, + { label: 'Subskryption', value: 'SUBSCRIPTION' }, + { label: 'Garantie', value: 'WARRANTY' }, + { label: 'Wartung', value: 'MAINTENANCE' }, + { label: 'Training', value: 'TRAINING' }, + ], + }, + ], + }, + noResults: { + title: 'Keine Produkte gefunden', + description: 'Es gibt keine Produkte, die Ihren Kriterien entsprechen', + }, + labels: { + clickToSelect: 'Klicken Sie, um auszuwählen', + gridView: 'Rasteransicht', + tableView: 'Tabellenansicht', + }, +}; + +const MOCK_PRODUCT_LIST_BLOCK_PL: CMS.Model.ProductListBlock.ProductListBlock = { + id: 'product-list-1', + title: 'Produkty', + subtitle: 'Przeglądaj nasz katalog produktów', + detailsLabel: 'Zobacz szczegóły', + products: { + data: [], + total: 0, + }, + table: { + columns: [ + { id: 'sku', title: 'SKU' }, + { id: 'name', title: 'Nazwa produktu' }, + { id: 'category', title: 'Kategoria' }, + { id: 'type', title: 'Typ' }, + { id: 'price', title: 'Cena' }, + ], + actions: { + title: 'Akcje', + label: 'Zobacz szczegóły', + }, + }, + pagination: { + limit: 12, + legend: 'z {totalPages} stron', + prev: 'Poprzednia', + next: 'Następna', + selectPage: 'Wybierz stronę', + }, + filters: { + label: 'Filtruj', + title: 'Filtruj Produkty', + description: 'Użyj filtrów, aby znaleźć konkretne produkty', + submit: 'Zastosuj Filtry', + reset: 'Resetuj Filtry', + removeFilters: 'Usuń filtry ({active})', + close: 'Zamknij filtry', + items: [ + { + __typename: 'FilterSelect', + id: 'sort', + label: 'Sortuj według', + allowMultiple: false, + options: [ + { label: 'Nazwa rosnąco', value: 'name_ASC' }, + { label: 'Nazwa malejąco', value: 'name_DESC' }, + { label: 'Cena rosnąco', value: 'price_ASC' }, + { label: 'Cena malejąco', value: 'price_DESC' }, + ], + }, + { + __typename: 'FilterSelect', + id: 'type', + label: 'Typ Produktu', + allowMultiple: true, + options: [ + { label: 'Fizyczny', value: 'PHYSICAL' }, + { label: 'Wirtualny', value: 'VIRTUAL' }, + ], + }, + { + __typename: 'FilterSelect', + id: 'category', + label: 'Kategoria', + allowMultiple: true, + options: [ + { label: 'Oprogramowanie', value: 'SOFTWARE' }, + { label: 'Narzędzia', value: 'TOOLS' }, + { label: 'Sprzęt', value: 'HARDWARE' }, + { label: 'Pomiar', value: 'MEASUREMENT' }, + { label: 'Chmura', value: 'CLOUD' }, + { label: 'Wsparcie', value: 'SUPPORT' }, + { label: 'Subskrypcja', value: 'SUBSCRIPTION' }, + { label: 'Gwarancja', value: 'WARRANTY' }, + { label: 'Utrzymanie', value: 'MAINTENANCE' }, + { label: 'Szkolenie', value: 'TRAINING' }, + ], + }, + ], + }, + noResults: { + title: 'Nie znaleziono produktów', + description: 'Nie znaleziono produktów spełniających Twoje kryteria', + }, + labels: { + clickToSelect: 'Kliknij, aby wybrać', + gridView: 'Widok siatki', + tableView: 'Widok tabeli', + }, +}; + +export const mapProductListBlock = (locale: string): CMS.Model.ProductListBlock.ProductListBlock => { + switch (locale) { + case 'en': + return MOCK_PRODUCT_LIST_BLOCK_EN; + case 'de': + return MOCK_PRODUCT_LIST_BLOCK_DE; + case 'pl': + return MOCK_PRODUCT_LIST_BLOCK_PL; + default: + return MOCK_PRODUCT_LIST_BLOCK_EN; + } +}; diff --git a/packages/integrations/mocked/src/modules/cms/mappers/cms.page.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/cms.page.mapper.ts index 1d6277f9c..9b3f07336 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/cms.page.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/cms.page.mapper.ts @@ -36,6 +36,7 @@ import { } from './mocks/pages/notification-list.page'; import { PAGE_ORDER_DETAILS_DE, PAGE_ORDER_DETAILS_EN, PAGE_ORDER_DETAILS_PL } from './mocks/pages/order-details.page'; import { PAGE_ORDER_LIST_DE, PAGE_ORDER_LIST_EN, PAGE_ORDER_LIST_PL } from './mocks/pages/order-list.page'; +import { PAGE_PRODUCT_LIST_DE, PAGE_PRODUCT_LIST_EN, PAGE_PRODUCT_LIST_PL } from './mocks/pages/product-list.page'; import { PAGE_SERVICE_DETAILS_DE, PAGE_SERVICE_DETAILS_EN, @@ -142,6 +143,13 @@ export const mapPage = (slug: string, locale: string): CMS.Model.Page.Page | und case '/uslugi': return PAGE_SERVICE_LIST_PL; + case '/products': + return PAGE_PRODUCT_LIST_EN; + case '/produkte': + return PAGE_PRODUCT_LIST_DE; + case '/produkty': + return PAGE_PRODUCT_LIST_PL; + case slug.match(/\/services\/.+/)?.[0]: return { ...PAGE_SERVICE_DETAILS_EN, @@ -267,6 +275,7 @@ export const getAllPages = (locale: string): CMS.Model.Page.Page[] => { PAGE_USER_ACCOUNT_PL, PAGE_SERVICE_LIST_PL, PAGE_SERVICE_DETAILS_PL, + PAGE_PRODUCT_LIST_PL, PAGE_CONTACT_US_PL, PAGE_COMPLAINT_FORM_PL, PAGE_REQUEST_DEVICE_MAINTENANCE_PL, @@ -288,6 +297,7 @@ export const getAllPages = (locale: string): CMS.Model.Page.Page[] => { PAGE_USER_ACCOUNT_DE, PAGE_SERVICE_LIST_DE, PAGE_SERVICE_DETAILS_DE, + PAGE_PRODUCT_LIST_DE, PAGE_CONTACT_US_DE, PAGE_COMPLAINT_FORM_DE, PAGE_REQUEST_DEVICE_MAINTENANCE_DE, @@ -309,6 +319,7 @@ export const getAllPages = (locale: string): CMS.Model.Page.Page[] => { PAGE_USER_ACCOUNT_EN, PAGE_SERVICE_LIST_EN, PAGE_SERVICE_DETAILS_EN, + PAGE_PRODUCT_LIST_EN, PAGE_CONTACT_US_EN, PAGE_COMPLAINT_FORM_EN, PAGE_REQUEST_DEVICE_MAINTENANCE_EN, @@ -334,6 +345,7 @@ export const getAlternativePages = (id: string, slug: string, locale: string): C PAGE_INVOICE_LIST_PL, PAGE_USER_ACCOUNT_PL, PAGE_SERVICE_LIST_PL, + PAGE_PRODUCT_LIST_PL, PAGE_DASHBOARD_DE, PAGE_TICKET_LIST_DE, PAGE_TICKET_DETAILS_DE, @@ -342,6 +354,7 @@ export const getAlternativePages = (id: string, slug: string, locale: string): C PAGE_INVOICE_LIST_DE, PAGE_USER_ACCOUNT_DE, PAGE_SERVICE_LIST_DE, + PAGE_PRODUCT_LIST_DE, PAGE_DASHBOARD_EN, PAGE_TICKET_LIST_EN, PAGE_TICKET_DETAILS_EN, @@ -350,6 +363,7 @@ export const getAlternativePages = (id: string, slug: string, locale: string): C PAGE_INVOICE_LIST_EN, PAGE_USER_ACCOUNT_EN, PAGE_SERVICE_LIST_EN, + PAGE_PRODUCT_LIST_EN, PAGE_SERVICE_DETAILS_EN, PAGE_SERVICE_DETAILS_DE, PAGE_SERVICE_DETAILS_PL, diff --git a/packages/integrations/mocked/src/modules/cms/mappers/mocks/pages/product-list.page.ts b/packages/integrations/mocked/src/modules/cms/mappers/mocks/pages/product-list.page.ts new file mode 100644 index 000000000..a91042cd8 --- /dev/null +++ b/packages/integrations/mocked/src/modules/cms/mappers/mocks/pages/product-list.page.ts @@ -0,0 +1,103 @@ +import { Auth, CMS } from '@o2s/framework/modules'; + +export const PAGE_PRODUCT_LIST_EN: CMS.Model.Page.Page = { + id: '20', + slug: '/products', + locale: 'en', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + seo: { + noIndex: false, + noFollow: false, + title: 'Products', + description: 'Browse our product catalog', + keywords: [], + image: { + url: 'https://picsum.photos/150', + width: 150, + height: 150, + alt: 'Placeholder', + }, + }, + permissions: [Auth.Constants.Roles.ORG_USER, Auth.Constants.Roles.ORG_ADMIN], + hasOwnTitle: false, + template: { + __typename: 'OneColumnTemplate', + slots: { + main: [ + { + __typename: 'ProductListBlock', + id: 'product-list-1', + }, + ], + }, + }, +}; + +export const PAGE_PRODUCT_LIST_DE: CMS.Model.Page.Page = { + id: '20', + slug: '/produkte', + locale: 'de', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + seo: { + noIndex: false, + noFollow: false, + title: 'Produkte', + description: 'Durchsuchen Sie unseren Produktkatalog', + keywords: [], + image: { + url: 'https://picsum.photos/150', + width: 150, + height: 150, + alt: 'Placeholder', + }, + }, + permissions: [Auth.Constants.Roles.ORG_USER, Auth.Constants.Roles.ORG_ADMIN], + hasOwnTitle: false, + template: { + __typename: 'OneColumnTemplate', + slots: { + main: [ + { + __typename: 'ProductListBlock', + id: 'product-list-1', + }, + ], + }, + }, +}; + +export const PAGE_PRODUCT_LIST_PL: CMS.Model.Page.Page = { + id: '20', + slug: '/produkty', + locale: 'pl', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + seo: { + noIndex: false, + noFollow: false, + title: 'Produkty', + description: 'Przeglądaj nasz katalog produktów', + keywords: [], + image: { + url: 'https://picsum.photos/150', + width: 150, + height: 150, + alt: 'Placeholder', + }, + }, + permissions: [Auth.Constants.Roles.ORG_USER, Auth.Constants.Roles.ORG_ADMIN], + hasOwnTitle: false, + template: { + __typename: 'OneColumnTemplate', + slots: { + main: [ + { + __typename: 'ProductListBlock', + id: 'product-list-1', + }, + ], + }, + }, +}; From e16751bae47cbaf3a0b938ae81ef02a43caf00f5 Mon Sep 17 00:00:00 2001 From: Aqil-Ahmad Date: Thu, 27 Nov 2025 17:51:09 +0500 Subject: [PATCH 2/4] feat(product-list): resolve comments --- .../api-harmonization/product-list.mapper.ts | 11 ++-- .../api-harmonization/product-list.model.ts | 1 + .../src/frontend/ProductList.client.tsx | 2 +- .../cms/models/blocks/product-list.model.ts | 36 ++--------- .../mappers/blocks/cms.product-list.mapper.ts | 60 ++++++++++++++++--- 5 files changed, 66 insertions(+), 44 deletions(-) diff --git a/packages/blocks/product-list/src/api-harmonization/product-list.mapper.ts b/packages/blocks/product-list/src/api-harmonization/product-list.mapper.ts index b95b35d21..4c924f9e2 100644 --- a/packages/blocks/product-list/src/api-harmonization/product-list.mapper.ts +++ b/packages/blocks/product-list/src/api-harmonization/product-list.mapper.ts @@ -16,16 +16,19 @@ export const mapProductList = ( pagination: cms.pagination, filters: cms.filters as unknown as ProductListBlock['filters'], table: cms.table as unknown as ProductListBlock['table'], + fieldMapping: cms.fieldMapping, noResults: cms.noResults, labels: cms.labels, products: { total: products.total, - data: products.data.map((product) => mapProduct(product)), + data: products.data.map((product) => mapProduct(product, cms)), }, }; }; -const mapProduct = (product: Products.Model.Product): ProductItem => { +const mapProduct = (product: Products.Model.Product, cms: CMS.Model.ProductListBlock.ProductListBlock): ProductItem => { + const { type, category } = cms.fieldMapping; + return { __typename: 'ProductItem', id: product.id, @@ -36,11 +39,11 @@ const mapProduct = (product: Products.Model.Product): ProductItem => { detailsUrl: product.link, type: { value: product.type, - label: product.type, + label: type?.[product.type] || product.type, }, category: { value: product.category, - label: product.category, + label: category?.[product.category] || product.category, }, price: product.price, image: product.image, diff --git a/packages/blocks/product-list/src/api-harmonization/product-list.model.ts b/packages/blocks/product-list/src/api-harmonization/product-list.model.ts index fac9f017f..1bb355c92 100644 --- a/packages/blocks/product-list/src/api-harmonization/product-list.model.ts +++ b/packages/blocks/product-list/src/api-harmonization/product-list.model.ts @@ -16,6 +16,7 @@ export class ProductListBlock extends ApiModels.Block.Block { total: number; }; table!: Models.DataTable.DataTable; + fieldMapping!: Models.Mapping.Mapping; noResults!: { title: string; description?: string; diff --git a/packages/blocks/product-list/src/frontend/ProductList.client.tsx b/packages/blocks/product-list/src/frontend/ProductList.client.tsx index 4d06d50e3..e9b74bb39 100644 --- a/packages/blocks/product-list/src/frontend/ProductList.client.tsx +++ b/packages/blocks/product-list/src/frontend/ProductList.client.tsx @@ -157,7 +157,7 @@ export const ProductListPure: React.FC = ({ locale, access ) : (
- + , 'id'> & { - id: ProductKeys; -}; - -type ProductDataTable = Omit, 'columns'> & { - columns: ProductTableColumn[]; -}; - -type ProductFilterSelect = Omit, 'id'> & { - id: ProductKeys; -}; - -type ProductFilterDateRange = Omit, 'id'> & { - id: ProductKeys; -}; - -type ProductFilterItem = ProductFilterSelect | ProductFilterDateRange; - -type ProductFilters = Omit, 'items'> & { - items: ProductFilterItem[]; -}; +import { Block, DataTable, Filters, Mapping, Pagination } from '@/utils/models'; export class ProductListBlock extends Block.Block { title!: string; subtitle?: string; - products!: Products; - table!: ProductDataTable; + table!: DataTable.DataTable; + fieldMapping!: Mapping.Mapping; pagination?: Pagination.Pagination; - filters?: ProductFilters; + filters?: Filters.Filters; noResults!: { title: string; - description: string; + description?: string; }; labels!: { clickToSelect: string; diff --git a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.product-list.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.product-list.mapper.ts index a26f87fc0..82f04d461 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.product-list.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.product-list.mapper.ts @@ -5,9 +5,23 @@ const MOCK_PRODUCT_LIST_BLOCK_EN: CMS.Model.ProductListBlock.ProductListBlock = title: 'Products', subtitle: 'Browse our product catalog', detailsLabel: 'View Details', - products: { - data: [], - total: 0, + fieldMapping: { + type: { + PHYSICAL: 'Physical', + VIRTUAL: 'Virtual', + }, + category: { + SOFTWARE: 'Software', + TOOLS: 'Tools', + HARDWARE: 'Hardware', + MEASUREMENT: 'Measurement', + CLOUD: 'Cloud', + SUPPORT: 'Support', + SUBSCRIPTION: 'Subscription', + WARRANTY: 'Warranty', + MAINTENANCE: 'Maintenance', + TRAINING: 'Training', + }, }, table: { columns: [ @@ -96,9 +110,23 @@ const MOCK_PRODUCT_LIST_BLOCK_DE: CMS.Model.ProductListBlock.ProductListBlock = title: 'Produkte', subtitle: 'Durchsuchen Sie unseren Produktkatalog', detailsLabel: 'Details anzeigen', - products: { - data: [], - total: 0, + fieldMapping: { + type: { + PHYSICAL: 'Physikalisch', + VIRTUAL: 'Virtuell', + }, + category: { + SOFTWARE: 'Software', + TOOLS: 'Werkzeuge', + HARDWARE: 'Hardware', + MEASUREMENT: 'Messung', + CLOUD: 'Cloud', + SUPPORT: 'Support', + SUBSCRIPTION: 'Abonnement', + WARRANTY: 'Garantie', + MAINTENANCE: 'Wartung', + TRAINING: 'Schulung', + }, }, table: { columns: [ @@ -187,9 +215,23 @@ const MOCK_PRODUCT_LIST_BLOCK_PL: CMS.Model.ProductListBlock.ProductListBlock = title: 'Produkty', subtitle: 'Przeglądaj nasz katalog produktów', detailsLabel: 'Zobacz szczegóły', - products: { - data: [], - total: 0, + fieldMapping: { + type: { + PHYSICAL: 'Fizyczny', + VIRTUAL: 'Wirtualny', + }, + category: { + SOFTWARE: 'Oprogramowanie', + TOOLS: 'Narzędzia', + HARDWARE: 'Sprzęt', + MEASUREMENT: 'Pomiar', + CLOUD: 'Chmura', + SUPPORT: 'Wsparcie', + SUBSCRIPTION: 'Subskrypcja', + WARRANTY: 'Gwarancja', + MAINTENANCE: 'Konserwacja', + TRAINING: 'Szkolenie', + }, }, table: { columns: [ From 0b3557960c96bab6a898aed7a4143c5bef035c9c Mon Sep 17 00:00:00 2001 From: Aqil-Ahmad Date: Thu, 27 Nov 2025 18:55:17 +0500 Subject: [PATCH 3/4] feat(product-list): add storybook --- .../frontend/ProductList.client.stories.tsx | 303 ++++++++++++++++++ .../mappers/blocks/cms.product-list.mapper.ts | 2 +- 2 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 packages/blocks/product-list/src/frontend/ProductList.client.stories.tsx diff --git a/packages/blocks/product-list/src/frontend/ProductList.client.stories.tsx b/packages/blocks/product-list/src/frontend/ProductList.client.stories.tsx new file mode 100644 index 000000000..29c973966 --- /dev/null +++ b/packages/blocks/product-list/src/frontend/ProductList.client.stories.tsx @@ -0,0 +1,303 @@ +import type { Meta, StoryObj } from '@storybook/nextjs'; + +import { ProductListPure } from './ProductList.client'; + +const meta = { + title: 'Blocks/ProductList', + component: ProductListPure, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + locale: 'en', + accessToken: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSmFuZSBEb2UiLCJlbWFpbCI6ImphbmVAZXhhbXBsZS5jb20iLCJyb2xlIjoic2VsZnNlcnZpY2Vfb3JnX2FkbWluIiwiY3VzdG9tZXIiOnsiaWQiOiJjdXN0LTAwMSIsInJvbGVzIjpbInNlbGZzZXJ2aWNlX29yZ191c2VyIiwic2VsZnNlcnZpY2Vfb3JnX3VzZXIiLCJzZWxmc2VydmljZV9vcmdfYWRtaW4iXSwibmFtZSI6IkFjbWUgQ29ycG9yYXRpb24ifSwiaWF0IjoxNzU2MzI0ODI5fQ.i-ofzMm1drdeK2_-ORFrv4YZZwadD_P-URxF_cMlkV4', + routing: { + locales: ['en', 'de', 'pl'], + defaultLocale: 'en', + pathnames: { + '/login': { + en: '/sign-in', + de: '/einloggen', + pl: '/logowanie', + }, + }, + }, + __typename: 'ProductListBlock', + id: 'product-list-1', + title: 'Products', + subtitle: 'Browse our product catalog', + detailsLabel: 'View Details', + table: { + columns: [ + { id: 'sku', title: 'SKU' }, + { id: 'name', title: 'Product Name' }, + { id: 'category', title: 'Category' }, + { id: 'type', title: 'Type' }, + { id: 'price', title: 'Price' }, + ], + actions: { + title: 'Actions', + label: 'View Details', + }, + }, + fieldMapping: { + type: { + PHYSICAL: 'Physical', + VIRTUAL: 'Virtual', + }, + category: { + SOFTWARE: 'Software', + TOOLS: 'Tools', + HARDWARE: 'Hardware', + MEASUREMENT: 'Measurement', + CLOUD: 'Cloud', + SUPPORT: 'Support', + SUBSCRIPTION: 'Subscription', + WARRANTY: 'Warranty', + MAINTENANCE: 'Maintenance', + TRAINING: 'Training', + }, + }, + pagination: { + limit: 12, + legend: 'of {totalPages} pages', + prev: 'Previous', + next: 'Next', + selectPage: 'Select page', + }, + filters: { + label: 'Filter', + title: 'Filter Products', + description: 'Use filters to find specific products', + submit: 'Apply Filters', + reset: 'Reset Filters', + removeFilters: 'Remove filters ({active})', + close: 'Close filters', + items: [ + { + __typename: 'FilterSelect', + id: 'sort', + label: 'Sort by', + allowMultiple: false, + options: [ + { label: 'Name ascending', value: 'name_ASC' }, + { label: 'Name descending', value: 'name_DESC' }, + { label: 'Price ascending', value: 'price_ASC' }, + { label: 'Price descending', value: 'price_DESC' }, + ], + }, + { + __typename: 'FilterSelect', + id: 'type', + label: 'Product Type', + allowMultiple: true, + options: [ + { label: 'Physical', value: 'PHYSICAL' }, + { label: 'Virtual', value: 'VIRTUAL' }, + ], + }, + { + __typename: 'FilterSelect', + id: 'category', + label: 'Category', + allowMultiple: true, + options: [ + { label: 'Software', value: 'SOFTWARE' }, + { label: 'Tools', value: 'TOOLS' }, + { label: 'Hardware', value: 'HARDWARE' }, + { label: 'Measurement', value: 'MEASUREMENT' }, + { label: 'Cloud', value: 'CLOUD' }, + { label: 'Support', value: 'SUPPORT' }, + { label: 'Subscription', value: 'SUBSCRIPTION' }, + { label: 'Warranty', value: 'WARRANTY' }, + { label: 'Maintenance', value: 'MAINTENANCE' }, + { label: 'Training', value: 'TRAINING' }, + ], + }, + ], + }, + noResults: { + title: 'No Products Found', + description: 'There are no products matching your criteria', + }, + labels: { + clickToSelect: 'Click to select', + gridView: 'Grid view', + tableView: 'Table view', + }, + products: { + total: 6, + data: [ + { + __typename: 'ProductItem', + id: 'PRD-001', + sku: 'PP-DRL-500', + name: 'PowerPro Drill 500W', + description: + 'Professional-grade cordless drill with 500W motor, perfect for heavy-duty applications', + shortDescription: 'Professional 500W cordless drill', + detailsUrl: '/products/powerpro-drill-500w', + type: { + value: 'PHYSICAL', + label: 'Physical', + }, + category: { + value: 'TOOLS', + label: 'Tools', + }, + price: { + value: 299.99, + currency: 'USD', + }, + image: { + url: 'https://raw.githubusercontent.com/o2sdev/openselfservice/refs/heads/main/packages/integrations/mocked/public/images/services-rental.jpg', + width: 640, + height: 656, + alt: 'PowerPro Drill 500W', + }, + tags: [ + { label: 'New', variant: 'secondary' }, + { label: 'Bestseller', variant: 'default' }, + ], + link: '/products/powerpro-drill-500w', + }, + { + __typename: 'ProductItem', + id: 'PRD-002', + sku: 'MC-LAS-CUT', + name: 'PrecisionCut Laser TS2', + description: + 'Advanced laser cutting system with dual CO₂ and fiber laser technology for industrial applications', + shortDescription: 'Industrial laser cutting system', + detailsUrl: '/products/precisioncut-laser-ts2', + type: { + value: 'PHYSICAL', + label: 'Physical', + }, + category: { + value: 'HARDWARE', + label: 'Hardware', + }, + price: { + value: 12999.0, + currency: 'USD', + }, + image: { + url: 'https://raw.githubusercontent.com/o2sdev/openselfservice/refs/heads/main/packages/integrations/mocked/public/images/services-charger.jpg', + width: 640, + height: 656, + alt: 'PrecisionCut Laser TS2', + }, + tags: [{ label: 'Professional', variant: 'default' }], + link: '/products/precisioncut-laser-ts2', + }, + { + __typename: 'ProductItem', + id: 'PRD-003', + sku: 'SW-CAD-PRO', + name: 'CAD Pro Design Suite', + description: '3D CAD software suite with advanced modeling and simulation capabilities', + shortDescription: 'Professional 3D CAD software', + detailsUrl: '/products/cad-pro-design-suite', + type: { + value: 'VIRTUAL', + label: 'Virtual', + }, + category: { + value: 'SOFTWARE', + label: 'Software', + }, + price: { + value: 499.0, + currency: 'USD', + }, + tags: [{ label: 'Digital', variant: 'secondary' }], + link: '/products/cad-pro-design-suite', + }, + { + __typename: 'ProductItem', + id: 'PRD-004', + sku: 'MS-LAS-100', + name: 'Laser Measurement Device', + description: 'High-precision laser distance measurement tool with 100m range', + shortDescription: 'Precision laser measurement', + detailsUrl: '/products/laser-measurement-device', + type: { + value: 'PHYSICAL', + label: 'Physical', + }, + category: { + value: 'MEASUREMENT', + label: 'Measurement', + }, + price: { + value: 189.99, + currency: 'USD', + }, + image: { + url: 'https://raw.githubusercontent.com/o2sdev/openselfservice/refs/heads/main/packages/integrations/mocked/public/images/services-calibration.jpg', + width: 640, + height: 656, + alt: 'Laser Measurement Device', + }, + tags: [{ label: 'New', variant: 'secondary' }], + link: '/products/laser-measurement-device', + }, + { + __typename: 'ProductItem', + id: 'PRD-005', + sku: 'CL-MON-360', + name: 'CloudMonitor 360', + description: 'Real-time cloud infrastructure monitoring and analytics platform', + shortDescription: 'Cloud monitoring solution', + detailsUrl: '/products/cloudmonitor-360', + type: { + value: 'VIRTUAL', + label: 'Virtual', + }, + category: { + value: 'CLOUD', + label: 'Cloud', + }, + price: { + value: 99.0, + currency: 'USD', + }, + tags: [ + { label: 'Subscription', variant: 'default' }, + { label: 'Popular', variant: 'secondary' }, + ], + link: '/products/cloudmonitor-360', + }, + { + __typename: 'ProductItem', + id: 'PRD-006', + sku: 'SUP-PRE-24', + name: 'Premium Support 24/7', + description: 'Round-the-clock premium technical support with guaranteed response time', + shortDescription: '24/7 premium support service', + detailsUrl: '/products/premium-support-247', + type: { + value: 'VIRTUAL', + label: 'Virtual', + }, + category: { + value: 'SUPPORT', + label: 'Support', + }, + price: { + value: 299.0, + currency: 'USD', + }, + tags: [{ label: 'Service', variant: 'default' }], + link: '/products/premium-support-247', + }, + ], + }, + }, +}; diff --git a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.product-list.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.product-list.mapper.ts index 82f04d461..cb32cc22d 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.product-list.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.product-list.mapper.ts @@ -191,7 +191,7 @@ const MOCK_PRODUCT_LIST_BLOCK_DE: CMS.Model.ProductListBlock.ProductListBlock = { label: 'Messung', value: 'MEASUREMENT' }, { label: 'Cloud', value: 'CLOUD' }, { label: 'Support', value: 'SUPPORT' }, - { label: 'Subskryption', value: 'SUBSCRIPTION' }, + { label: 'Abonnement', value: 'SUBSCRIPTION' }, { label: 'Garantie', value: 'WARRANTY' }, { label: 'Wartung', value: 'MAINTENANCE' }, { label: 'Training', value: 'TRAINING' }, From b47d36aca3ca131df46b4deb53f01988fc2c2ddd Mon Sep 17 00:00:00 2001 From: Aqil-Ahmad Date: Fri, 28 Nov 2025 18:33:12 +0500 Subject: [PATCH 4/4] fix(product-list): minor fix --- .../src/modules/cms/mappers/blocks/cms.product-list.mapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.product-list.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.product-list.mapper.ts index cb32cc22d..f05e090f2 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.product-list.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.product-list.mapper.ts @@ -298,7 +298,7 @@ const MOCK_PRODUCT_LIST_BLOCK_PL: CMS.Model.ProductListBlock.ProductListBlock = { label: 'Wsparcie', value: 'SUPPORT' }, { label: 'Subskrypcja', value: 'SUBSCRIPTION' }, { label: 'Gwarancja', value: 'WARRANTY' }, - { label: 'Utrzymanie', value: 'MAINTENANCE' }, + { label: 'Konserwacja', value: 'MAINTENANCE' }, { label: 'Szkolenie', value: 'TRAINING' }, ], },