diff --git a/cloud/alwatr/keep/compose.yml b/cloud/alwatr/keep/compose.yml index 8febf66d9..9b98fae22 100644 --- a/cloud/alwatr/keep/compose.yml +++ b/cloud/alwatr/keep/compose.yml @@ -104,3 +104,38 @@ services: traefik.frontend.rule: Host:${DOMAIN}; PathPrefix:/api/v0/form/ traefik.frontend.entryPoints: http, https traefik.frontend.redirect.entryPoint: https + + cdn: + image: ghcr.io/alimd/nginx:${CDN_VERSION} + + cpu_count: 1 + cpu_shares: 1024 + mem_limit: 256m + + restart: unless-stopped + + networks: + alwatr-public-network: + + environment: + - NGINX_ACCESS_LOG=${CDN_ACCESS_LOG:-off} + + volumes: + - type: bind + source: ./_data/cdn + target: /var/www/html/cdn + bind: + create_host_path: true + + logging: + driver: json-file + options: + max-size: '10m' + max-file: '2' + + labels: + traefik.enable: true + traefik.port: 80 + traefik.frontend.rule: Host:${DOMAIN}; PathPrefix:/cdn/ + traefik.frontend.entryPoints: http, https + traefik.frontend.redirect.entryPoint: https diff --git a/cloud/alwatr/keep/env/keep-beta.env b/cloud/alwatr/keep/env/keep-beta.env index 08af97ad7..fe86d7523 100644 --- a/cloud/alwatr/keep/env/keep-beta.env +++ b/cloud/alwatr/keep/env/keep-beta.env @@ -1,11 +1,13 @@ -U2FsdGVkX185BxaW3WchKkEJMHXAhXV4Z12U+dvZOvD3JYeMf7wgfi2cekkFWq6H -nMFD/dSciHQ1dDkIVpjoOlkr+vlU0+TxFX13hCjZqwov8lrYLlHG4X8pLaELriBH -SR1vwuw7j5VMHRfgrh7FKgyI0xDZydq0XPStICdFuh5orNgjDOZc8nyoTxq7iWV9 -waJxKqRe+yPT5pHX3fpB4N0DwY9xfBQ6VztOaP6MB3BEQKV5n4Um6YalF8GSOULV -lNbMxwA2laQTltJzN1KSCEtlTCbZkr5epUGM0AlB/aN9fcWV+x299gj29KaJkHgo -O8x7nhvAsFYhmEUVpIorUNcEu4jGxKHkUDOZY6k8KKjrX6ieI9tdj/waPqd+AGv1 -3CxPnXB843Zvp7cQ7J/fLvfXXnO6PkXUyAhpr10TveUdx8+OIkvzp3pj9pW3ATJm -/ox3KKbXvAUzAkFAnCERuoh6x5HWARoFUV2wzZA4TJXCqT7nnA6Wz17LY5SD+aHF -wDZFqyfwSmxVKEq+i/uu0+21NgDUoC+WXG+NBfTTznq5ZxHRpZ1YOTOJFtwQLqDv -FQ3rCwgJ18ATB6jeLsJ8VbR3gdiu2hb0XdvCijLRxgADQ2hKiMKYEAAT/piz3rFH -tWFqbZ+9ZWfJDaIRYBDWduKZ6bituXjtzKW6Gr12PQiyC30JVxBvdq8X82e+oJ+H +U2FsdGVkX19CMnOE6hyCgHsbGjyTKXzAX7I4d68iExuxhbWs6rVFLt4JCBC7zJ1Q +S4WPsk3Y/ULKcjcmUjRcw5n65si1CAdnOEYV7sQNXa3kHCa0YrGF/SmBWf8tUrvu +ho2EmqvLQUm3Ogdj9K/MBTY2L5pxOypdU8+mRP6zvrGtxM//q+I6SOg72TouHNwb +pf1VkXyB7eGwJTnFPrBadqEHHn6Wg7LmhpdfErxU2os1d30lk0elzQowm0T5dODL +vBpblC6s6V6XaL0gziqXw2V/LZaL++BQziSI5+V+cF18/G6ghR1Qzdm0wTRzHa/E +svs8KZuh7Gb17RCykKQ8oJ8yvf6YWIxVRmaxtO1xhpRw6xRSMN7dxiyizBXiK6pa +AQORte5lY2KPcTtCmsTRIlb3oJvYTufMys6SF1F9dL2T1Mu8ncXWnwuXqRWwNzXz +Spr3zvjz9/laHAXR/DidA3+ALs9u8E5Edox4p/eYHQBRi9WDkW1+3TF/7o404Oc6 +7p58MpbhqLkzQZXDWl6WBhwq1AFjLm5/Asj4qgWZsV9KA1lBNEi3RcMoq0JHEmIf +qIkhglY8cc83uOak/xRZIxiSVLrLQ1n7Mpc2FmppCuGaVUu4i82KKw3gsgMa73vU +cWtvVX8Dgia8WKdI1Oig7hDtBCmwmUhlSWJvLdeP1y6Nq2S3yHBXXeGU1ENJnUfS +6ZU4ppQwdueRk4ICOsEL1lX7wN3qq1coKdTYaGV0IV33KxeCjhO5A3NipnrjY+vV +FTHp+nYPYy3Su/ZbpNPtSg== diff --git a/cloud/alwatr/keep/env/keep.env b/cloud/alwatr/keep/env/keep.env index 3de08a59b..da34bd941 100644 --- a/cloud/alwatr/keep/env/keep.env +++ b/cloud/alwatr/keep/env/keep.env @@ -1,11 +1,12 @@ -U2FsdGVkX1/pduxljtvI8MU7n7p576dWYQhoisET5kWl7s+YS4Xny4xhxtX8VAGl -3EhJ9r/3fzZ5MdC+OoTy+6eq/QHpk/JLiwr7JrTaBLvl5sOQQs2shBJ1XDgrQfim -Cn+vDvP/KHlnMBz67LpnptxYsWK0kXhSm5doj3ZsbwL7YDeH5zYZgslpkA+dpUlw -INA995Ssx7CfJXqslHcR0Pktywu6vkznvz1md6GW3foZ7GR+VEsE1y8F3ovQRW9+ -RVQuJY2y6tOz1LYsL1nzBg+RtYF3z4xdlDGMrKLTsph/jz2iAU+SZgbpGMc6tH39 -/astVIiDvlnip/t1OYupLDH8puWvoNmdx3/rSYh6/SayBk2EHdXWFvcuYp04t+lo -1kBozcuQdP5ERpc7Kb5cJgn4daQlaE/OMSrPElUNqp8O9fMrMelTVW0LIeq+eZ/2 -ncAgcZjDG8KyNniPKwi/4hYXMMF6BAWU8Wj7uMkSQQEFIuATHh62OmkONSahPaKn -dMBOY9JVerinduRnC7RkFtAbBsuLC7RqoGcr68ZgyZaCxwoAf3Vdkb61q4sLzgUb -7Rydk6by+CqYvdL6cxYUDji/UTfTgHN5yEqmDrMDlIyeNxLpTDOmfaQjlXsRklkU -V7OPlYG2f498HkYPWyDU4dcI5Hoj/Hwl5p3rGF9u5b4= +U2FsdGVkX1+VyXOwL/nBIs5KJPzfHylH6KUreqAuPqzpIr1AQsju/g9rC47Tj9Cf +BDeNjOd2YWKIJlY+DizhyOHRwNGu7QVihT2V9VSVZs6n/c2kJlbeGNQfrdZt6aK3 +63eZtVHn7fby1mcNw/VJ71L4AHVuj83HuzTS3ZMr1HzAerjG+W90uhZNxOetFFYQ +86GUCoVkHFBzyC5eW9gBKNB0rPM5wo0tjwdMwchEh0bgkjRiTjzI97ZwkGpJt9vc +QZJMdnq1lNumvmMrNwcICOCNpsw2cu3f6WSA2H9/qbAmbX5KLyWaXOKlXx/+7K0a +q7yANjrD7J8SVvySPaTbyUBkOiPpUrFfMAjR23FAVUTSCaqjYX7naFXdrM6GJLxG +JjREAwDFYyRq0Z4gV04ethcctJnf8v7jE9LOvhb+liFpUI/6idMWaJl5WuT4+nmy +KN6vAW08VTWotMc++Lli8KG5bIr/8ByRj/QgL5BfJrn9VL36c5nYHko2+UT1CRur +TOInVxiIcEBxB8uHBZdQqwQCkdRoz0kFFhLg1z6IgMDZ0ilRk57v4dHNJk4dF3uz +ZLz3O2pzCyZ5Uw4yn83Dam8hY72bhrKMVJC+5fdoPye5etKSrr6SgW953SybNG7n +NTJf5E4qCxavMECr3YgCKyVGoHTEnnoiJd+XC7ShiTDtdf3Cav7wYRcGwx34aAOU +A0JNgrZq9Ii2pdVgDukCDsjBbY3W1Nfd4WE/UfZjAk3hBv2K/CJJQpNBU1RUlVM4 diff --git a/core/type/src/photo.ts b/core/type/src/photo.ts index 349040241..0c7920c94 100644 --- a/core/type/src/photo.ts +++ b/core/type/src/photo.ts @@ -11,5 +11,5 @@ export type Photo = AlwatrDocumentObject & { /** * Photo extra meta information for future maintenances */ - meta: Record; // meta: {order: 1233, customer: 1334} + meta?: Record; // meta: {order: 1233, customer: 1334} }; diff --git a/uniquely/keep-pwa/package.json b/uniquely/keep-pwa/package.json index 15e056506..67902b6cd 100644 --- a/uniquely/keep-pwa/package.json +++ b/uniquely/keep-pwa/package.json @@ -39,22 +39,20 @@ "devDependencies": { "@alwatr/element": "^0.32.0", "@alwatr/fetch": "^0.32.0", - "@alwatr/fsm": "^0.32.0", - "@alwatr/context": "^0.32.0", "@alwatr/i18n": "^0.32.0", "@alwatr/math": "^0.32.0", "@alwatr/pwa-helper": "^0.32.0", - "@alwatr/router": "^0.32.0", "@alwatr/signal": "^0.32.0", "@alwatr/type": "^0.32.0", "@alwatr/ui-kit": "^0.32.0", - "@web/dev-server": "^0.2.1", + "@alwatr/util": "^0.32.0", + "@web/dev-server": "^0.2.3", "@webcomponents/webcomponentsjs": "^2.8.0", - "esbuild": "^0.17.18", + "esbuild": "^0.18.2", "lit-analyzer": "^1.2.1", "npm-run-all": "^4.1.5", "ts-lit-plugin": "^1.2.1", - "tslib": "^2.5.0", - "typescript": "^5.0.4" + "tslib": "^2.5.3", + "typescript": "^5.1.3" } } diff --git a/uniquely/keep-pwa/src/config.ts b/uniquely/keep-pwa/src/config.ts index dd9c0dee8..6e8fb0501 100644 --- a/uniquely/keep-pwa/src/config.ts +++ b/uniquely/keep-pwa/src/config.ts @@ -2,23 +2,26 @@ import {FetchOptions} from '@alwatr/fetch'; import {getConfKey} from '@alwatr/pwa-helper/config.js'; import {getLocalStorageItem} from '@alwatr/util'; -const token = getConfKey('token'); - /** * Debug API. * * ```ts - * localStorage.setItem('DEBUG_API', '"https://canary.keeperco.ir"'); + * localStorage.setItem('DEBUG_API', '"https://canary.keeperco.ir/"'); * localStorage.setItem('DEBUG_CONFIG', JSON.stringify({token: 'secret_token'})); * ``` */ -const apiPrefix = getLocalStorageItem('DEBUG_API', ''); +const srvBaseUrl = getLocalStorageItem('DEBUG_API', '/'); +const apiBaseUrl = srvBaseUrl + 'api/v0/'; + export const config = { - cdn: apiPrefix + '/cdn', - api: apiPrefix + '/api/v0', - token, + serverContext: { + base: srvBaseUrl, + api: apiBaseUrl, + cdn: srvBaseUrl + 'cdn', + token: getConfKey('token'), + }, + fetchContextOptions: >{ - method: 'GET', removeDuplicate: 'auto', retry: 2, retryDelay: 2_000, diff --git a/uniquely/keep-pwa/src/content/home-page-fa.ts b/uniquely/keep-pwa/src/content/home-page-fa.ts index 25212a8ad..5eefc61f8 100644 --- a/uniquely/keep-pwa/src/content/home-page-fa.ts +++ b/uniquely/keep-pwa/src/content/home-page-fa.ts @@ -31,6 +31,7 @@ export const homePageContent: PageHomeContent = { wide: true, icon: 'cart-outline', headline: 'مشاهده محصولات', + href: '/product', }, catalogue: { elevated: 1, diff --git a/uniquely/keep-pwa/src/content/l18e-en.json b/uniquely/keep-pwa/src/content/l18e-en.json index 1632eca67..217b64ecb 100644 --- a/uniquely/keep-pwa/src/content/l18e-en.json +++ b/uniquely/keep-pwa/src/content/l18e-en.json @@ -23,6 +23,7 @@ "form_submitted": "Your request has been submitted.", "collaboration_form_description": "Collaboration request form with Keep", "invalid_form_data": "The form information is not correct.", - "check_network_connection": "Please check internet, and send again." + "check_network_connection": "Please check internet, and send again.", + "page_product_headline": "Products" } } diff --git a/uniquely/keep-pwa/src/content/l18e-fa.json b/uniquely/keep-pwa/src/content/l18e-fa.json index 03ad2a5b3..54df3f804 100644 --- a/uniquely/keep-pwa/src/content/l18e-fa.json +++ b/uniquely/keep-pwa/src/content/l18e-fa.json @@ -26,6 +26,7 @@ "form_submitted": "درخواست شما ثبت شد.", "collaboration_form_description": "فرم درخواست همکاری با کیپ", "invalid_form_data": "اطلاعات فرم صحیح نمی‌باشد.", - "check_network_connection": "لطفا از اتصال خود به اینترنت اطمینان حاصل فرمایید و دوباره ارسال کنید." + "check_network_connection": "لطفا از اتصال خود به اینترنت اطمینان حاصل فرمایید و دوباره ارسال کنید.", + "page_product_headline": "محصولات" } } diff --git a/uniquely/keep-pwa/src/manager/submit-form-command-handler.ts b/uniquely/keep-pwa/src/manager/submit-form-command-handler.ts index 5af83ca70..d7eab6b85 100644 --- a/uniquely/keep-pwa/src/manager/submit-form-command-handler.ts +++ b/uniquely/keep-pwa/src/manager/submit-form-command-handler.ts @@ -41,11 +41,11 @@ commandHandler.define(submitFormCommandTrigger.id, async (for try { await serviceRequest({ method: 'PUT', - url: config.api + '/form/', + url: config.serverContext.api + 'form/', queryParameters: { formId: form.formId, }, - token: config.token, + token: config.serverContext.token, bodyJson, }); } diff --git a/uniquely/keep-pwa/src/ui/alwatr-pwa.ts b/uniquely/keep-pwa/src/ui/alwatr-pwa.ts index 848b6ec5a..2b4572088 100644 --- a/uniquely/keep-pwa/src/ui/alwatr-pwa.ts +++ b/uniquely/keep-pwa/src/ui/alwatr-pwa.ts @@ -27,6 +27,10 @@ class AlwatrPwa extends AlwatrPwaElement { 'home': () => { return html`...`; }, + 'product': () => { + import('./page/product.js'); + return html`...`; + }, '_404': () => { import('./page/404.js'); return html`...`; diff --git a/uniquely/keep-pwa/src/ui/page/404.ts b/uniquely/keep-pwa/src/ui/page/404.ts index 426d013b5..6d767a892 100644 --- a/uniquely/keep-pwa/src/ui/page/404.ts +++ b/uniquely/keep-pwa/src/ui/page/404.ts @@ -10,7 +10,7 @@ import { import {message} from '@alwatr/i18n'; import '@alwatr/ui-kit/card/icon-box.js'; -import {topAppBarContextProvider} from '../../manager/context.js'; +import {languageButtonClickEventListener, topAppBarContextProvider} from '../../manager/context.js'; import type {IconBoxContent} from '@alwatr/ui-kit/card/icon-box.js'; @@ -38,8 +38,9 @@ export class AlwatrPage404 extends UnresolvedMixin(LocalizeMixin(SignalMixin(Alw super.connectedCallback(); topAppBarContextProvider.setValue({ type: 'small', - headline: message('page_404_not_found'), + headlineKey: 'page_404_not_found', startIcon: {icon: 'arrow-back-outline', flipRtl: true, clickSignalId: 'back_to_home_click_event'}, + endIconList: [{icon: 'globe-outline', clickSignalId: languageButtonClickEventListener.id}], tinted: 2, }); } diff --git a/uniquely/keep-pwa/src/ui/page/product.ts b/uniquely/keep-pwa/src/ui/page/product.ts new file mode 100644 index 000000000..2711a75c3 --- /dev/null +++ b/uniquely/keep-pwa/src/ui/page/product.ts @@ -0,0 +1,51 @@ +import { + customElement, + css, + html, + LocalizeMixin, + SignalMixin, + AlwatrBaseElement, + UnresolvedMixin, +} from '@alwatr/element'; + +import {languageButtonClickEventListener, topAppBarContextProvider} from '../../manager/context.js'; +import '../stuff/select-product.js'; + + +declare global { + interface HTMLElementTagNameMap { + 'alwatr-page-product': AlwatrPageProduct; + } +} + +/** + * Alwatr Select Product Page + */ +@customElement('alwatr-page-product') +export class AlwatrPageProduct extends UnresolvedMixin(LocalizeMixin(SignalMixin(AlwatrBaseElement))) { + static override styles = css` + :host { + display: block; + padding: calc(2 * var(--sys-spacing-track)); + box-sizing: border-box; + min-height: 100%; + } + `; + + override connectedCallback(): void { + super.connectedCallback(); + topAppBarContextProvider.setValue({ + type: 'small', + headlineKey: 'page_product_headline', + startIcon: {icon: 'arrow-back-outline', flipRtl: true, clickSignalId: 'back_to_home_click_event'}, + endIconList: [{icon: 'globe-outline', clickSignalId: languageButtonClickEventListener.id}], + tinted: 2, + }); + } + + override render(): unknown { + this._logger.logMethod?.('render'); + + return html``; + } +} diff --git a/uniquely/keep-pwa/src/ui/stuff/product-card.ts b/uniquely/keep-pwa/src/ui/stuff/product-card.ts new file mode 100644 index 000000000..6bb8f76db --- /dev/null +++ b/uniquely/keep-pwa/src/ui/stuff/product-card.ts @@ -0,0 +1,137 @@ +import { + css, + customElement, + SignalMixin, + LocalizeMixin, + nothing, + html, + property, + type PropertyValues, +} from '@alwatr/element'; +import {number} from '@alwatr/i18n'; +import '@alwatr/icon'; +import '@alwatr/ui-kit/button/icon-button.js'; +import {AlwatrSurface} from '@alwatr/ui-kit/card/surface.js'; + + +import type {StringifyableRecord} from '@alwatr/type'; + +declare global { + interface HTMLElementTagNameMap { + 'alwatr-product-card2': AlwatrProductCard; + } +} + +export interface ProductCartContent extends StringifyableRecord { + id?: string; + title: string; + imagePath: string; + price?: number; +} + +/** + * Alwatr not selectable elevated card element. + */ +@customElement('alwatr-product-card2') +export class AlwatrProductCard extends LocalizeMixin(SignalMixin(AlwatrSurface)) { + static override styles = [ + AlwatrSurface.styles, + css` + :host { + display: block; + padding: 0; + user-select: none; + -webkit-user-select: none; + --_surface-color-on: var(--sys-color-on-surface-hsl); + --_surface-color-bg: var(--sys-color-surface-hsl); + outline: 2px solid transparent; + } + + img { + display: block; + box-sizing: border-box; + width: 100%; + min-height: 50px; + height: auto; + border-radius: 0 0 var(--sys-radius-medium) var(--sys-radius-medium); + filter: brightness(1); + transition: filter var(--sys-motion-duration-small) var(--sys-motion-easing-normal); + } + + @media (prefers-color-scheme: dark) { + img { + filter: brightness(0.8); + } + } + + .content{ + padding: calc(2 * var(--sys-spacing-track)); + + } + .title { + margin: 0; + /* text-align: center; */ + font-family: var(--sys-typescale-headline-small-font-family-name); + font-weight: var(--sys-typescale-headline-small-font-weight); + font-size: var(--sys-typescale-headline-small-font-size); + letter-spacing: var(--sys-typescale-headline-small-letter-spacing); + line-height: var(--sys-typescale-headline-small-line-height); + margin-bottom: var(--sys-spacing-track); + } + + .price { + text-align: end; + color: var(--sys-color-on-surface-variant); + font-family: var(--sys-typescale-body-small-font-family-name); + font-weight: var(--sys-typescale-body-small-font-weight); + font-size: var(--sys-typescale-body-small-font-size); + letter-spacing: var(--sys-typescale-body-small-letter-spacing); + line-height: var(--sys-typescale-body-small-line-height); + } + + .price ins { + color: var(--sys-color-primary); + text-decoration: none; + font-weight: var(--ref-font-weight-medium); + vertical-align: middle; + } + + svg { + display: inline-block; + width: 1em; + height: 1em; + contain: strict; + vertical-align: middle; + } + `, + ]; + + @property({type: Object, attribute: false}) + content?: ProductCartContent; + + override connectedCallback(): void { + super.connectedCallback(); + this.setAttribute('stated', ''); + this.setAttribute('elevated', ''); + } + + protected override shouldUpdate(changedProperties: PropertyValues): boolean { + return super.shouldUpdate(changedProperties) && this.content != null; + } + + override render(): unknown { + this._logger.logMethod?.('render'); + if (this.content == null) return nothing; + + return html` + ${this.content.title} +
+

${this.content.title}

+
+ ${number(this.content.price)} + +
+
+ `; + } +} diff --git a/uniquely/keep-pwa/src/ui/stuff/select-product.ts b/uniquely/keep-pwa/src/ui/stuff/select-product.ts new file mode 100644 index 000000000..4504edda8 --- /dev/null +++ b/uniquely/keep-pwa/src/ui/stuff/select-product.ts @@ -0,0 +1,98 @@ +import { + AlwatrBaseElement, + LocalizeMixin, + SignalMixin, + UnresolvedMixin, + css, + customElement, + html, + mapObject, + property, +} from '@alwatr/element'; +import {localeContextConsumer} from '@alwatr/i18n'; +import '@alwatr/ui-kit/button/button.js'; + +import {config} from '../../config.js'; +import '../stuff/product-card.js'; + +import type {ProductCartContent} from '../stuff/product-card.js'; +import type {AlwatrDocumentStorage} from '@alwatr/type'; +import type {Product} from '@alwatr/type/customer-order-management.js'; + +declare global { + interface HTMLElementTagNameMap { + 'alwatr-select-product': AlwatrSelectProduct; + } +} + +/** + * Alwatr Select Product Element. + */ +@customElement('alwatr-select-product') +export class AlwatrSelectProduct extends LocalizeMixin(SignalMixin(UnresolvedMixin(AlwatrBaseElement))) { + static override styles = css` + :host { + box-sizing: border-box; + display: flex; + flex-wrap: wrap; + /* padding: var(--sys-spacing-track) calc(2 * var(--sys-spacing-track)); */ + gap: var(--sys-spacing-track); + box-sizing: border-box; + justify-content: flex-end; + } + + alwatr-product-card2 { + width: 100%; + flex-grow: 1; + } + `; + + @property() + protected productStorage?: AlwatrDocumentStorage = { + ok: true, + meta: { + id: 'publistore/hub/product-list/scope', + formatVersion: 5, + reversion: 1, + lastUpdated: 1684866396314, + lastAutoId: -1, + }, + data: { + '1': { + id: '1', + title: {fa: 'اسکوپ دوشیار زاویه', en: 'Double-Angle Scoop'}, + image: {id: 'keep-scope-3-0.jpg'}, + price: 1590, + }, + '2': { + id: '2', + title: {fa: 'اسکوپ پروانه', en: 'Helical Scoop'}, + image: {id: 'keep-scope-1-0.jpg'}, + price: 1400, + }, + '3': { + id: '3', + title: {fa: 'اسکوپ دوشیار صاف', en: 'Double-Sided Flat Scoop'}, + image: {id: 'keep-scope-2-0.jpg'}, + price: 1590, + }, + }, + }; + + override render(): unknown { + this._logger.logMethod?.('render'); + + return mapObject(this, this.productStorage?.data, this.render_part_product_card); + } + + protected render_part_product_card(product: Product & {price: number}): unknown { + const langCode = (localeContextConsumer.getValue()?.language ?? 'fa') as Lowercase; + const content: ProductCartContent = { + id: product.id, + title: product.title[langCode] ?? product.title.fa, + imagePath: config.serverContext.cdn + '/large/' + product.image.id, + price: product.price, + }; + return html``; + } +}