diff --git a/.github/workflows/publish-alwatr-container.yml b/.github/workflows/publish-alwatr-container.yml index d839a4ec7..d5fd2edf1 100644 --- a/.github/workflows/publish-alwatr-container.yml +++ b/.github/workflows/publish-alwatr-container.yml @@ -71,6 +71,11 @@ jobs: dockerfile: cloud/container/alwatr-pwa.dockerfile description: Customer Order Management PWA. + - image: keep-pwa + source: uniquely/keep-pwa + dockerfile: cloud/container/alwatr-pwa.dockerfile + description: Keep PWA. + - image: com-api source: uniquely/com-api dockerfile: cloud/container/alwatr-services.dockerfile diff --git a/cloud/alwatr/keep/compose.yml b/cloud/alwatr/keep/compose.yml new file mode 100644 index 000000000..8febf66d9 --- /dev/null +++ b/cloud/alwatr/keep/compose.yml @@ -0,0 +1,106 @@ +networks: + internal: + alwatr-public-network: + name: alwatr-public-network + external: true + +services: + pwa: + image: ghcr.io/alimd/keep-pwa:${PWA_VERSION} + + cpu_count: 1 + cpu_shares: 1024 + mem_limit: 256m + + restart: unless-stopped + + networks: + alwatr-public-network: + + logging: + driver: json-file + options: + max-size: '10m' + max-file: '2' + + environment: + - NGINX_ACCESS_LOG=${PWA_ACCESS_LOG:-off} + - API_ACCESS_TOKEN=${API_ACCESS_TOKEN} + + labels: + traefik.enable: true + traefik.port: 80 + traefik.frontend.rule: Host:${DOMAIN}; PathPrefix:/ + traefik.frontend.entryPoints: http, https + traefik.frontend.redirect.entryPoint: https + + storage-server: + image: ghcr.io/alimd/storage-server:${STORAGE_SERVER_VERSION} + + cpu_count: 1 + cpu_shares: 1024 + mem_limit: 256m + + stop_grace_period: 30s + + restart: unless-stopped + + networks: + internal: + aliases: + - '${APP_ID}-storage-server' + + environment: + - STORAGE_PATH=/storage + - ACCESS_TOKEN=${STORAGE_TOKEN} + - ALWATR_DEBUG=${STORAGE_SERVER_DEBUG:-} + + volumes: + - type: bind + source: ./_data/storage + target: /storage + bind: + create_host_path: true + + logging: + driver: json-file + options: + max-size: '10m' + max-file: '2' + + form-registration: + image: ghcr.io/alimd/form-registration:${FORM_REGISTRATION_VERSION} + + cpu_count: 1 + cpu_shares: 1024 + mem_limit: 256m + + restart: unless-stopped + + networks: + internal: + alwatr-public-network: + + depends_on: + - storage-server + + environment: + - STORAGE_HOST=${APP_ID}-storage-server + - STORAGE_PORT=80 + - STORAGE_TOKEN=${STORAGE_TOKEN} + - ACCESS_TOKEN=${API_ACCESS_TOKEN} + - FORM_LIST=${FORM_REGISTRATION_LIST} + - ALWATR_DEBUG=${FORM_REGISTRATION_DEBUG:-} + + logging: + driver: json-file + options: + max-size: '10m' + max-file: '2' + + labels: + traefik.enable: true + traefik.port: 80 + traefik.frontend.rule: Host:${DOMAIN}; PathPrefix:/api/v0/form/ + traefik.frontend.entryPoints: http, https + traefik.frontend.redirect.entryPoint: https diff --git a/cloud/alwatr/keep/deploy.sh b/cloud/alwatr/keep/deploy.sh new file mode 100755 index 000000000..978ccd900 --- /dev/null +++ b/cloud/alwatr/keep/deploy.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +source ../../classic/lib/deploy.sh diff --git a/cloud/alwatr/keep/env/keep-beta.env b/cloud/alwatr/keep/env/keep-beta.env new file mode 100644 index 000000000..08af97ad7 --- /dev/null +++ b/cloud/alwatr/keep/env/keep-beta.env @@ -0,0 +1,11 @@ +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 diff --git a/cloud/alwatr/keep/env/keep.env b/cloud/alwatr/keep/env/keep.env new file mode 100644 index 000000000..3de08a59b --- /dev/null +++ b/cloud/alwatr/keep/env/keep.env @@ -0,0 +1,11 @@ +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= diff --git a/tsconfig.json b/tsconfig.json index f84a3158a..c8f6c9467 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -52,6 +52,7 @@ {"path": "./uniquely/soffit-pwa"}, {"path": "./uniquely/com-api"}, {"path": "./uniquely/com-pwa"}, + {"path": "./uniquely/keep-pwa"}, // labs {"path": "./labs/zx-example"} diff --git a/ui/ui-kit/style/theme/palette-40.css b/ui/ui-kit/style/theme/palette-40.css new file mode 100644 index 000000000..650abebc8 --- /dev/null +++ b/ui/ui-kit/style/theme/palette-40.css @@ -0,0 +1,111 @@ +:root { + /* HCT 40 (keep #f06627) */ + + /* primary */ + --ref-palette-primary0: 0, 0%, 0%; + --ref-palette-primary10: 15.3, 100%, 10.8%; + --ref-palette-primary20: 18.7, 100%, 17.6%; + --ref-palette-primary25: 19.4, 100%, 21.2%; + --ref-palette-primary30: 20.3, 100%, 24.9%; + --ref-palette-primary35: 20.8, 100%, 28.8%; + --ref-palette-primary40: 21.3, 100%, 32.5%; + --ref-palette-primary50: 20.4, 89.8%, 42.4%; + --ref-palette-primary60: 18.8, 87%, 54.7%; + --ref-palette-primary70: 17.4, 100%, 68.2%; + --ref-palette-primary80: 16.5, 100%, 80%; + --ref-palette-primary90: 15.9, 100%, 90.4%; + --ref-palette-primary95: 15, 100%, 95.3%; + --ref-palette-primary98: 13.3, 100%, 98.2%; + --ref-palette-primary99: 300, 100%, 99.2%; + --ref-palette-primary100: 0, 0%, 100%; + + /* secondary */ + --ref-palette-secondary0: 0, 0%, 0%; + --ref-palette-secondary10: 17.4, 54.4%, 11.2%; + --ref-palette-secondary20: 16.7, 36%, 19.6%; + --ref-palette-secondary25: 17.4, 31.1%, 23.9%; + --ref-palette-secondary30: 16.5, 27.4%, 28.6%; + --ref-palette-secondary35: 15.7, 24.7%, 33.3%; + --ref-palette-secondary40: 16.4, 22.7%, 38%; + --ref-palette-secondary50: 15.3, 19.2%, 48%; + --ref-palette-secondary60: 16.8, 23.4%, 58%; + --ref-palette-secondary70: 16.7, 33.7%, 68.6%; + --ref-palette-secondary80: 16.1, 53.8%, 79.6%; + --ref-palette-secondary90: 15.9, 100%, 90.4%; + --ref-palette-secondary95: 15, 100%, 95.3%; + --ref-palette-secondary98: 13.3, 100%, 98.2%; + --ref-palette-secondary99: 300, 100%, 99.2%; + --ref-palette-secondary100: 0, 0%, 100%; + + /* tertiary */ + --ref-palette-tertiary0: 0, 0%, 0%; + --ref-palette-tertiary10: 49.1, 100%, 6.5%; + --ref-palette-tertiary20: 50.4, 80.6%, 12.2%; + --ref-palette-tertiary25: 49.6, 61.9%, 16.5%; + --ref-palette-tertiary30: 50.9, 50.5%, 20.6%; + --ref-palette-tertiary35: 49.1, 42.6%, 25.3%; + --ref-palette-tertiary40: 49.3, 36.8%, 29.8%; + --ref-palette-tertiary50: 49, 30%, 39.2%; + --ref-palette-tertiary60: 49.5, 25.3%, 48.8%; + --ref-palette-tertiary70: 48.4, 32.1%, 59%; + --ref-palette-tertiary80: 48.9, 44.9%, 69.4%; + --ref-palette-tertiary90: 48.5, 72.3%, 80.2%; + --ref-palette-tertiary95: 48.2, 100%, 86.1%; + --ref-palette-tertiary98: 41.1, 100%, 96.3%; + --ref-palette-tertiary99: 300, 100%, 99.2%; + --ref-palette-tertiary100: 0, 0%, 100%; + + /* neutral */ + --ref-palette-neutral0: 0, 0%, 0%; + --ref-palette-neutral10: 15, 14.3%, 11%; + --ref-palette-neutral20: 18, 10.2%, 19.2%; + --ref-palette-neutral25: 18, 8.3%, 23.5%; + --ref-palette-neutral30: 16.4, 7.7%, 28%; + --ref-palette-neutral35: 16.4, 6.6%, 32.7%; + --ref-palette-neutral40: 10.9, 5.8%, 37.5%; + --ref-palette-neutral50: 13.8, 5.4%, 47.3%; + --ref-palette-neutral60: 12.9, 6.4%, 57.3%; + --ref-palette-neutral70: 16, 9.1%, 67.6%; + --ref-palette-neutral80: 15, 14.5%, 78.4%; + --ref-palette-neutral90: 14.1, 32.1%, 89.6%; + --ref-palette-neutral95: 14.1, 68%, 95.1%; + --ref-palette-neutral98: 13.3, 100%, 98.2%; + --ref-palette-neutral99: 300, 100%, 99.2%; + --ref-palette-neutral100: 0, 0%, 100%; + + /* neutral-variant */ + --ref-palette-neutral-variant0: 0, 0%, 0%; + --ref-palette-neutral-variant10: 17.6, 29.8%, 11.2%; + --ref-palette-neutral-variant20: 15.8, 19.2%, 19.4%; + --ref-palette-neutral-variant25: 15, 16.4%, 23.9%; + --ref-palette-neutral-variant30: 14.3, 14.5%, 28.4%; + --ref-palette-neutral-variant35: 16.4, 13.1%, 32.9%; + --ref-palette-neutral-variant40: 16.4, 11.5%, 37.6%; + --ref-palette-neutral-variant50: 15, 9.9%, 47.5%; + --ref-palette-neutral-variant60: 16.2, 12%, 57.6%; + --ref-palette-neutral-variant70: 15.6, 16.6%, 68%; + --ref-palette-neutral-variant80: 14.5, 27.1%, 79%; + --ref-palette-neutral-variant90: 15.5, 60.8%, 90%; + --ref-palette-neutral-variant95: 15, 100%, 95.3%; + --ref-palette-neutral-variant98: 13.3, 100%, 98.2%; + --ref-palette-neutral-variant99: 300, 100%, 99.2%; + --ref-palette-neutral-variant100: 0, 0%, 100%; + + /* error */ + --ref-palette-error0: 0, 0%, 0%; + --ref-palette-error10: 358.2, 100%, 12.7%; + --ref-palette-error20: 357.1, 100%, 20.6%; + --ref-palette-error25: 356.7, 100%, 24.7%; + --ref-palette-error30: 355.9, 100%, 28.8%; + --ref-palette-error35: 356.6, 92%, 34.3%; + --ref-palette-error40: 0, 75.5%, 41.6%; + --ref-palette-error50: 2.4, 72.5%, 52.9%; + --ref-palette-error60: 3.6, 100%, 64.3%; + --ref-palette-error70: 5.5, 100%, 74.5%; + --ref-palette-error80: 6.4, 100%, 83.5%; + --ref-palette-error90: 5.9, 100%, 92%; + --ref-palette-error95: 8.6, 100%, 95.9%; + --ref-palette-error98: 7.5, 100%, 98.4%; + --ref-palette-error99: 300, 100%, 99.2%; + --ref-palette-error100: 0, 0%, 100%; +} diff --git a/uniquely/keep-pwa/README.md b/uniquely/keep-pwa/README.md new file mode 100644 index 000000000..c77822be8 --- /dev/null +++ b/uniquely/keep-pwa/README.md @@ -0,0 +1 @@ +# Keep PWA diff --git a/uniquely/keep-pwa/esbuild.mjs b/uniquely/keep-pwa/esbuild.mjs new file mode 100755 index 000000000..45cb0f24a --- /dev/null +++ b/uniquely/keep-pwa/esbuild.mjs @@ -0,0 +1,2 @@ +#!/usr/bin/env node +import '@alwatr/pwa-helper/esbuild.mjs'; diff --git a/uniquely/keep-pwa/package.json b/uniquely/keep-pwa/package.json new file mode 100644 index 000000000..15e056506 --- /dev/null +++ b/uniquely/keep-pwa/package.json @@ -0,0 +1,60 @@ +{ + "name": "@alwatr/keep-pwa", + "version": "0.32.0", + "description": "Alwatr Keep PWA.", + "type": "module", + "author": "S. Ali Mihandoost (https://ali.mihandoost.com)", + "contributors": [ + "S. Amir Mohammad Najafi (https://njfamirm.ir)" + ], + "private": true, + "engines": { + "node": ">=18.13.0", + "npm": ">=8.0.0", + "yarn": ">=1.22.0" + }, + "browserslist": "> 0.1%, not dead", + "repository": { + "type": "git", + "url": "https://github.com/AliMD/alwatr", + "directory": "uniquely/keep-pwa" + }, + "scripts": { + "b": "yarn build-dev", + "cb": "run-s clean build-dev", + "s": "yarn start", + "w": "yarn watch", + "start": "NODE_OPTIONS=--enable-source-maps run-s clean build serve", + "clean": "rm -rf dist build .tsbuildinfo", + "build": "yarn build:es", + "build-dev": "run-s build:root build:tsc \"build:es --debug\"", + "build:es": "./esbuild.mjs", + "build:tsc": "tsc --build", + "build:root": "cd ../../ && yarn build:ts", + "serve": "wds", + "watch": "run-p watch:root watch:es serve", + "watch:es": "yarn build:es --clean --watch --debug", + "watch:root": "cd ../../ && yarn watch:ts" + }, + "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", + "@webcomponents/webcomponentsjs": "^2.8.0", + "esbuild": "^0.17.18", + "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" + } +} diff --git a/uniquely/keep-pwa/res/config.js b/uniquely/keep-pwa/res/config.js new file mode 100644 index 000000000..63a20519d --- /dev/null +++ b/uniquely/keep-pwa/res/config.js @@ -0,0 +1,3 @@ +window.appConfig = { + token: '$API_ACCESS_TOKEN', +} diff --git a/uniquely/keep-pwa/res/image/favicon.ico b/uniquely/keep-pwa/res/image/favicon.ico new file mode 100644 index 000000000..f7974d955 Binary files /dev/null and b/uniquely/keep-pwa/res/image/favicon.ico differ diff --git a/uniquely/keep-pwa/res/image/icon-192-maskable.png b/uniquely/keep-pwa/res/image/icon-192-maskable.png new file mode 100644 index 000000000..1c8ed01d9 Binary files /dev/null and b/uniquely/keep-pwa/res/image/icon-192-maskable.png differ diff --git a/uniquely/keep-pwa/res/image/icon-192.png b/uniquely/keep-pwa/res/image/icon-192.png new file mode 100644 index 000000000..a0e5990e2 Binary files /dev/null and b/uniquely/keep-pwa/res/image/icon-192.png differ diff --git a/uniquely/keep-pwa/res/image/icon-512-maskable.png b/uniquely/keep-pwa/res/image/icon-512-maskable.png new file mode 100644 index 000000000..f28c0035b Binary files /dev/null and b/uniquely/keep-pwa/res/image/icon-512-maskable.png differ diff --git a/uniquely/keep-pwa/res/image/icon-512.png b/uniquely/keep-pwa/res/image/icon-512.png new file mode 100644 index 000000000..d01867d0f Binary files /dev/null and b/uniquely/keep-pwa/res/image/icon-512.png differ diff --git a/uniquely/keep-pwa/res/image/keep.svg b/uniquely/keep-pwa/res/image/keep.svg new file mode 100644 index 000000000..dab29e1c0 --- /dev/null +++ b/uniquely/keep-pwa/res/image/keep.svg @@ -0,0 +1 @@ + diff --git a/uniquely/keep-pwa/res/index.html b/uniquely/keep-pwa/res/index.html new file mode 100644 index 000000000..cc9af3e37 --- /dev/null +++ b/uniquely/keep-pwa/res/index.html @@ -0,0 +1,80 @@ + + + + + + + Keep Collection + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + درحال بارگذاری... + + + + + + diff --git a/uniquely/keep-pwa/res/manifest.webmanifest b/uniquely/keep-pwa/res/manifest.webmanifest new file mode 100644 index 000000000..0b1707e8a --- /dev/null +++ b/uniquely/keep-pwa/res/manifest.webmanifest @@ -0,0 +1,19 @@ +{ + "short_name": "Keep", + "name": "Keep Collection", + "description": "Keep Collection Official PWA", + "start_url": "/", + "display": "standalone", + "theme_color": "#c24000", + "background_color": "#fffcfa", + "orientation": "portrait", + "default_locale": "fa", + "icons": [ + {"src": "/image/icon-192-maskable.png", "type": "image/png", "sizes": "192x192", "purpose": "maskable"}, + {"src": "/image/icon-512-maskable.png", "type": "image/png", "sizes": "512x512", "purpose": "maskable"}, + {"src": "/image/favicon.ico", "type": "image/x-icon", "sizes": "32x32"}, + {"src": "/image/icon-192.png", "type": "image/png", "sizes": "192x192"}, + {"src": "/image/icon-512.png", "type": "image/png", "sizes": "512x512"} + ], + "screenshots": [] +} diff --git a/uniquely/keep-pwa/res/robots.txt b/uniquely/keep-pwa/res/robots.txt new file mode 100644 index 000000000..c2a49f4fb --- /dev/null +++ b/uniquely/keep-pwa/res/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / diff --git a/uniquely/keep-pwa/res/service-worker.js b/uniquely/keep-pwa/res/service-worker.js new file mode 100644 index 000000000..64a5d005d --- /dev/null +++ b/uniquely/keep-pwa/res/service-worker.js @@ -0,0 +1 @@ +console.log('Service worker not build in development mode.') diff --git a/uniquely/keep-pwa/src/alwatr-pwa.ts b/uniquely/keep-pwa/src/alwatr-pwa.ts new file mode 100644 index 000000000..2fa476453 --- /dev/null +++ b/uniquely/keep-pwa/src/alwatr-pwa.ts @@ -0,0 +1,3 @@ +// TODO: rename me to index +import './manager/index.js'; +import './ui/alwatr-pwa.js'; diff --git a/uniquely/keep-pwa/src/config.ts b/uniquely/keep-pwa/src/config.ts new file mode 100644 index 000000000..dd9c0dee8 --- /dev/null +++ b/uniquely/keep-pwa/src/config.ts @@ -0,0 +1,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_CONFIG', JSON.stringify({token: 'secret_token'})); + * ``` + */ +const apiPrefix = getLocalStorageItem('DEBUG_API', ''); +export const config = { + cdn: apiPrefix + '/cdn', + api: apiPrefix + '/api/v0', + token, + fetchContextOptions: >{ + method: 'GET', + removeDuplicate: 'auto', + retry: 2, + retryDelay: 2_000, + }, +} as const; diff --git a/uniquely/keep-pwa/src/content/home-page-en.ts b/uniquely/keep-pwa/src/content/home-page-en.ts new file mode 100644 index 000000000..f9016f5a8 --- /dev/null +++ b/uniquely/keep-pwa/src/content/home-page-en.ts @@ -0,0 +1,95 @@ +/* eslint-disable max-len */ +import {languageButtonClickEventListener} from '../manager/context.js'; + +import type {PageHomeContent} from '../type.js'; + +export const homePageContent: PageHomeContent = { + topAppBar: { + type: 'center', + headline: 'Keep Collection', + startIcon: {icon: 'menu-outline', flipRtl: true}, + endIconList: [{icon: 'globe-outline', clickSignalId: languageButtonClickEventListener.id}], + }, + about: { + elevated: 1, + stated: true, + wide: true, + headline: 'About Us', + slot: ` + Keep is an Iranian holding company in the field of building materials and stone veneer manufacturing. It is engaged in the production and distribution of construction services.
+ The factory operates with the aim of distributing construction products in Iranian and international markets.
+ The products produced by this collection are of high quality and are accompanied by experienced staff committed to customer satisfaction. + `, + }, + product: { + elevated: 1, + highlight: true, + stated: true, + wide: true, + icon: 'cart-outline', + headline: 'View Products', + }, + catalogue: { + elevated: 1, + highlight: true, + stated: true, + icon: 'cloud-download-outline', + flipRtl: true, + headline: 'Download Catalogue', + description: 'Download the catalogue introducing Keep products', + href: 'https://www.dropbox.com/s/dl/k1jct7jc7netmsq/keep-product-brochure.pdf', + target: 'download', + }, + socialList: [ + { + elevated: 1, + highlight: true, + stated: true, + icon: 'call-outline', + flipRtl: true, + headline: 'Contact Us', + slot: '
Direct Contact Number
0915 101 1507
', + href: 'tel:+989151011507', + }, + { + elevated: 1, + highlight: true, + stated: true, + icon: 'send-outline', + headline: 'Telegram', + description: 'Keep Telegram Channel', + href: 'https://t.me/keeperco', + target: '_blank', + }, + { + elevated: 1, + highlight: true, + stated: true, + icon: 'logo-instagram', + headline: 'Instagram', + description: 'Keep Instagram Page', + href: 'https://instagram.com/keeperco', + target: '_blank', + }, + { + elevated: 1, + highlight: true, + stated: true, + icon: 'logo-youtube', + headline: 'Youtube', + description: 'Keep Youtube channel', + href: 'https://www.youtube.com/keeperco', + target: '_blank', + }, + { + elevated: 1, + highlight: true, + stated: true, + icon: 'videocam-outline', + headline: 'Aparat', + description: 'Keep Aparat Channel', + href: 'https://www.aparat.com/keeperco', + target: '_blank', + }, + ], +}; diff --git a/uniquely/keep-pwa/src/content/home-page-fa.ts b/uniquely/keep-pwa/src/content/home-page-fa.ts new file mode 100644 index 000000000..25212a8ad --- /dev/null +++ b/uniquely/keep-pwa/src/content/home-page-fa.ts @@ -0,0 +1,101 @@ +/* eslint-disable max-len */ + +import {replaceNumber} from '@alwatr/i18n'; + +import {languageButtonClickEventListener} from '../manager/context.js'; + +import type {PageHomeContent} from '../type.js'; + +export const homePageContent: PageHomeContent = { + topAppBar: { + type: 'center', + headline: 'مجموعه کیپ', + startIcon: {icon: 'menu-outline', flipRtl: true}, + endIconList: [{icon: 'globe-outline', clickSignalId: languageButtonClickEventListener.id}], + }, + about: { + elevated: 1, + stated: true, + wide: true, + headline: 'درباره ما', + slot: ` + شرکت تولیدی کیپ مصالح ساختمانی و اسکوپ سنگ‌نما هلدینگی ایرانی است که در زمینه تولید و توزیع خدمات ساختمانی فعالیت دارد.
+ این کارخانه با هدف توزیع محصولات ساختمانی در بازارهای ایرانی و خارجی مشغول به فعالیت می‌باشد.
+ محصولات تولیدی این مجموعه با کیفیت بالا و کادری مجرب و تعهد به اصول جلب رضایت مشتری می‌باشد. + `, + }, + product: { + elevated: 1, + highlight: true, + stated: true, + wide: true, + icon: 'cart-outline', + headline: 'مشاهده محصولات', + }, + catalogue: { + elevated: 1, + highlight: true, + stated: true, + icon: 'cloud-download-outline', + flipRtl: true, + headline: 'دانلود کاتالوگ', + description: 'دانلود کاتالوگ معرفی محصولات کیپ', + href: 'https://www.dropbox.com/s/dl/k1jct7jc7netmsq/keep-product-brochure.pdf', + target: 'download', + }, + socialList: [ + { + elevated: 1, + highlight: true, + stated: true, + icon: 'call-outline', + flipRtl: true, + headline: 'تماس با ما', + slot: ` +
تلفن ارتباط مستقیم
+
${replaceNumber('0915 101 1507')}
+ `, + href: 'tel:+989151011507', + }, + { + elevated: 1, + highlight: true, + stated: true, + icon: 'send-outline', + headline: 'تلگرام', + description: 'کانال تلگرام کیپ', + href: 'https://t.me/keeperco', + target: '_blank', + }, + { + elevated: 1, + highlight: true, + stated: true, + icon: 'logo-instagram', + headline: 'اینستاگرام', + description: 'صفحه‌ی اینستاگرام کیپ', + href: 'https://instagram.com/keep_scope/', + target: '_blank', + }, + { + elevated: 1, + highlight: true, + stated: true, + icon: 'logo-youtube', + headline: 'یوتیوب', + description: 'کانال یوتیوب کیپ', + href: 'https://www.youtube.com/keeperco', + target: '_blank', + }, + { + elevated: 1, + highlight: true, + stated: true, + icon: 'videocam-outline', + headline: 'آپارات', + description: 'کانال آپارات کیپ', + href: 'https://www.aparat.com/keeperco', + target: '_blank', + }, + ], +}; diff --git a/uniquely/keep-pwa/src/content/l18e-en.json b/uniquely/keep-pwa/src/content/l18e-en.json new file mode 100644 index 000000000..1632eca67 --- /dev/null +++ b/uniquely/keep-pwa/src/content/l18e-en.json @@ -0,0 +1,28 @@ +{ + "ok": true, + "meta": { + "code": "en-US", + "rev": 0 + }, + "data": { + "loading": "Loading...", + "app_footer_description": "The largest stone scope manufacturer in the eastern part of the country", + "page_404_not_found": "Not Found!", + "page_404_not_found_description": "Sorry, the requested page was not found!", + "full_name": "Full Name", + "phone_number": "Phone Number", + "city": "City", + "activity_field": "Activity Field", + "tool_distribution": "Tool Distributor", + "sales_tools": "Sales Tools", + "stonemason": "Stonemason", + "contractor": "Contractor", + "submit_form": "Submit Form", + "cancel": "Cancel", + "collaboration_form_title": "Collaboration Request", + "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." + } +} diff --git a/uniquely/keep-pwa/src/content/l18e-fa.json b/uniquely/keep-pwa/src/content/l18e-fa.json new file mode 100644 index 000000000..03ad2a5b3 --- /dev/null +++ b/uniquely/keep-pwa/src/content/l18e-fa.json @@ -0,0 +1,31 @@ +{ + "ok": true, + "meta": { + "code": "fa-IR", + "rev": 0 + }, + "data": { + "loading": "در حال بارگذاری ...", + + "app_footer_description": "بزرگترین تولید کننده اسکوپ سنگ در شرق کشور", + + "page_404_not_found": "یافت نشد!", + "page_404_not_found_description": "متاسفانه صفحه مورد نظر یافت نشد!", + + "full_name": "نام و نام‌خانوادگی", + "phone_number": "شماره تلفن", + "city": "شهر", + "activity_field": "حوزه فعالیت", + "tool_distribution": "توزیع‌کننده ابزار", + "sales_tools": "فروشنده ابزار", + "stonemason": "سنگ‌کار", + "contractor": "پیمانکار", + "submit_form": "ارسال فرم", + "cancel": "لغو", + "collaboration_form_title": "درخواست همکاری", + "form_submitted": "درخواست شما ثبت شد.", + "collaboration_form_description": "فرم درخواست همکاری با کیپ", + "invalid_form_data": "اطلاعات فرم صحیح نمی‌باشد.", + "check_network_connection": "لطفا از اتصال خود به اینترنت اطمینان حاصل فرمایید و دوباره ارسال کنید." + } +} diff --git a/uniquely/keep-pwa/src/manager/change-language.ts b/uniquely/keep-pwa/src/manager/change-language.ts new file mode 100644 index 000000000..a539b6fd6 --- /dev/null +++ b/uniquely/keep-pwa/src/manager/change-language.ts @@ -0,0 +1,19 @@ +import {localeContextConsumer, setLocale} from '@alwatr/i18n'; + +import {languageButtonClickEventListener} from './context.js'; +import {logger} from './logger.js'; + +languageButtonClickEventListener.subscribe(() => { + logger.logMethod?.('changeLanguage'); + localeContextConsumer.getValue()?.language === 'en' + ? setLocale({ + code: 'fa-IR', + language: 'fa', + direction: 'rtl', + }) + : setLocale({ + code: 'en-US', + language: 'en', + direction: 'ltr', + }); +}); diff --git a/uniquely/keep-pwa/src/manager/context-provider/home-page-content.ts b/uniquely/keep-pwa/src/manager/context-provider/home-page-content.ts new file mode 100644 index 000000000..074e7a5ef --- /dev/null +++ b/uniquely/keep-pwa/src/manager/context-provider/home-page-content.ts @@ -0,0 +1,14 @@ +import {localeContextConsumer} from '@alwatr/i18n'; + +import {homePageContent} from '../../content/home-page-fa.js'; // for perf +import {homePageContentContextProvider} from '../context.js'; +import {logger} from '../logger.js'; + +localeContextConsumer.subscribe(async () => { + const language = localeContextConsumer.getValue()?.language; + logger.logMethodArgs?.('contentProvider', {language}); + if (language == null) return; + + const content = language === 'en' ? (await import('../../content/home-page-en.js')).homePageContent : homePageContent; + homePageContentContextProvider.setValue(content); +}); diff --git a/uniquely/keep-pwa/src/manager/context-provider/l18e.ts b/uniquely/keep-pwa/src/manager/context-provider/l18e.ts new file mode 100644 index 000000000..f832c58e0 --- /dev/null +++ b/uniquely/keep-pwa/src/manager/context-provider/l18e.ts @@ -0,0 +1,17 @@ +import {setL18eLoader} from '@alwatr/i18n'; + +import l18eFa from '../../content/l18e-fa.json' assert {type: 'json'}; // for perf +import {logger} from '../logger.js'; + +import type {L18eContext} from '@alwatr/type'; + +setL18eLoader((locale) => { + const language = locale.language; + logger.logMethodArgs?.('l18eLoader', {language}); + if (language === 'en') { + return import('../../content/l18e-en.json', {assert: {type: 'json'}}) as unknown as Promise; + } + else { + return l18eFa as unknown as Promise; + } +}); diff --git a/uniquely/keep-pwa/src/manager/context.ts b/uniquely/keep-pwa/src/manager/context.ts new file mode 100644 index 000000000..629e6cccb --- /dev/null +++ b/uniquely/keep-pwa/src/manager/context.ts @@ -0,0 +1,15 @@ +import {commandTrigger, contextConsumer, contextProvider, eventListener} from '@alwatr/signal'; + +import type {PageHomeContent, FormData} from '../type.js'; +import type {ClickSignalType} from '@alwatr/type'; + +export * from '@alwatr/pwa-helper/context.js'; + +export const homePageContentContextProvider = + contextProvider.bind('home_page_content_context'); +export const homePageContentContextConsumer = + contextConsumer.bind(homePageContentContextProvider.id); + +export const languageButtonClickEventListener = eventListener.bind('language_button_click_event'); + +export const submitFormCommandTrigger = commandTrigger.bind('submit_form_command'); diff --git a/uniquely/keep-pwa/src/manager/index.ts b/uniquely/keep-pwa/src/manager/index.ts new file mode 100644 index 000000000..da75263c6 --- /dev/null +++ b/uniquely/keep-pwa/src/manager/index.ts @@ -0,0 +1,4 @@ +import './change-language.js'; +import './context-provider/home-page-content.js'; +import './context-provider/l18e.js'; +import './submit-form-command-handler.js'; diff --git a/uniquely/keep-pwa/src/manager/logger.ts b/uniquely/keep-pwa/src/manager/logger.ts new file mode 100644 index 000000000..e15c6c8ce --- /dev/null +++ b/uniquely/keep-pwa/src/manager/logger.ts @@ -0,0 +1,3 @@ +import {createLogger} from '@alwatr/logger'; + +export const logger = createLogger('keep-manager'); diff --git a/uniquely/keep-pwa/src/manager/submit-form-command-handler.ts b/uniquely/keep-pwa/src/manager/submit-form-command-handler.ts new file mode 100644 index 000000000..5af83ca70 --- /dev/null +++ b/uniquely/keep-pwa/src/manager/submit-form-command-handler.ts @@ -0,0 +1,59 @@ +import {serviceRequest} from '@alwatr/fetch'; +import {commandHandler} from '@alwatr/signal'; +import {snackbarSignalTrigger} from '@alwatr/ui-kit/snackbar/show-snackbar.js'; +import {validator, type JsonSchema} from '@alwatr/validator'; + +import {submitFormCommandTrigger} from './context.js'; +import {logger} from './logger.js'; +import {config} from '../config.js'; + +import type {FormData} from '../type.js'; + +const validSchema: Record = { + 'collaboration': {name: String, phone: Number, city: String, activity: String}, +}; + +commandHandler.define(submitFormCommandTrigger.id, async (form: FormData): Promise => { + logger.logMethodArgs?.('formSubmit', {form}); + + let bodyJson; + try { + const schema = validSchema[form.formId]; + + if (schema == null) { + logger.accident( + 'formSubmit', + 'invalid_form_id', + 'Please define form id in validSchema before use it', + {formId: form.formId, validSchema: Object.keys(validSchema)}, + ); + return false; + } + + bodyJson = validator(schema, form.data); + } + catch (err) { + logger.accident('formSubmit', 'invalid_form_data', 'validator failed on form data', (err as Error).cause); + snackbarSignalTrigger.request({messageKey: 'invalid_form_data'}); + return false; + } + + try { + await serviceRequest({ + method: 'PUT', + url: config.api + '/form/', + queryParameters: { + formId: form.formId, + }, + token: config.token, + bodyJson, + }); + } + catch (err) { + logger.error('formSubmit', 'request_failed', (err as Error).cause); + snackbarSignalTrigger.request({messageKey: 'check_network_connection'}); + return false; + } + + return true; +}); diff --git a/uniquely/keep-pwa/src/type.ts b/uniquely/keep-pwa/src/type.ts new file mode 100644 index 000000000..9ec3342fb --- /dev/null +++ b/uniquely/keep-pwa/src/type.ts @@ -0,0 +1,21 @@ +import type {StringifyableRecord} from '@alwatr/type'; +import type {IconBoxContent} from '@alwatr/ui-kit/card/icon-box.js'; +import type {TopAppBarContent} from '@alwatr/ui-kit/top-app-bar/top-app-bar.js'; + +export interface FormData extends StringifyableRecord { + formId: string; + data: Record; +} + +export interface BoxType extends IconBoxContent { + wide?: boolean; + slot?: string; +} + +export interface PageHomeContent extends StringifyableRecord { + topAppBar: TopAppBarContent; + about: BoxType; + catalogue: BoxType; + product: BoxType; + socialList: Array; +} diff --git a/uniquely/keep-pwa/src/ui/alwatr-pwa.ts b/uniquely/keep-pwa/src/ui/alwatr-pwa.ts new file mode 100644 index 000000000..848b6ec5a --- /dev/null +++ b/uniquely/keep-pwa/src/ui/alwatr-pwa.ts @@ -0,0 +1,40 @@ +import {html, customElement} from '@alwatr/element'; +import '@alwatr/font/vazirmatn.css'; +import {AlwatrPwaElement} from '@alwatr/pwa-helper/pwa-element.js'; +import '@alwatr/ui-kit/style/mobile-only.css'; +import '@alwatr/ui-kit/style/theme/color.css'; +import '@alwatr/ui-kit/style/theme/palette-40.css'; + +import './page/home.js'; // for perf +import './stuff/app-footer.js'; + +import type {RoutesConfig} from '@alwatr/router'; + +declare global { + interface HTMLElementTagNameMap { + 'alwatr-pwa': AlwatrPwa; + } +} + +/** + * Alwatr PWA Root Element + */ +@customElement('alwatr-pwa') +class AlwatrPwa extends AlwatrPwaElement { + protected override _routesConfig: RoutesConfig = { + routeId: (routeContext) => routeContext.sectionList[0]?.toString(), + templates: { + 'home': () => { + return html`...`; + }, + '_404': () => { + import('./page/404.js'); + return html`...`; + }, + }, + }; + + protected override _navigationBarTemplate(): unknown { + return html``; + } +} diff --git a/uniquely/keep-pwa/src/ui/page/404.ts b/uniquely/keep-pwa/src/ui/page/404.ts new file mode 100644 index 000000000..426d013b5 --- /dev/null +++ b/uniquely/keep-pwa/src/ui/page/404.ts @@ -0,0 +1,62 @@ +import { + customElement, + css, + html, + LocalizeMixin, + SignalMixin, + AlwatrBaseElement, + UnresolvedMixin, +} from '@alwatr/element'; +import {message} from '@alwatr/i18n'; +import '@alwatr/ui-kit/card/icon-box.js'; + +import {topAppBarContextProvider} from '../../manager/context.js'; + +import type {IconBoxContent} from '@alwatr/ui-kit/card/icon-box.js'; + +declare global { + interface HTMLElementTagNameMap { + 'alwatr-page-404': AlwatrPage404; + } +} + +/** + * Alwatr 404 Page + */ +@customElement('alwatr-page-404') +export class AlwatrPage404 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', + headline: message('page_404_not_found'), + startIcon: {icon: 'arrow-back-outline', flipRtl: true, clickSignalId: 'back_to_home_click_event'}, + tinted: 2, + }); + } + + override render(): unknown { + this._logger.logMethod?.('render'); + + const box: IconBoxContent = { + stated: true, + elevated: 1, + icon: 'construct-outline', + flipRtl: true, + headline: message('page_404_not_found'), + description: message('page_404_not_found_description'), + preLine: true, + }; + + return html``; + } +} diff --git a/uniquely/keep-pwa/src/ui/page/home.ts b/uniquely/keep-pwa/src/ui/page/home.ts new file mode 100644 index 000000000..266cc16f3 --- /dev/null +++ b/uniquely/keep-pwa/src/ui/page/home.ts @@ -0,0 +1,89 @@ +import { + customElement, + css, + html, + unsafeHTML, + state, + nothing, + SignalMixin, + AlwatrBaseElement, + mapIterable, + UnresolvedMixin, +} from '@alwatr/element'; +import {message} from '@alwatr/i18n'; +import '@alwatr/ui-kit/card/icon-box.js'; + +import {homePageContentContextConsumer, topAppBarContextProvider} from '../../manager/context.js'; +import '../stuff/collaboration-box.js'; + +import type {BoxType, PageHomeContent} from '../../type.js'; + +declare global { + interface HTMLElementTagNameMap { + 'alwatr-page-home': AlwatrPageHome; + } +} + +/** + * Alwatr Keep Home Page + */ +@customElement('alwatr-page-home') +export class AlwatrPageHome extends UnresolvedMixin(SignalMixin(AlwatrBaseElement)) { + static override styles = css` + :host { + display: flex; + flex-wrap: wrap; + padding: calc(2 * var(--sys-spacing-track)); + justify-content: center; + gap: var(--sys-spacing-track); + overflow-y: auto; + } + + alwatr-icon-box { + width: 40%; + flex-grow: 1; + } + + .logo { + width: 90%; + margin-bottom: var(--sys-spacing-track); + } + + alwatr-icon-box[wide], + alwatr-collaboration-box { + width: 100%; + } + `; + + @state() content?: PageHomeContent; + + override connectedCallback(): void { + super.connectedCallback(); + + this._addSignalListeners( + homePageContentContextConsumer.subscribe((content) => { + this.content = content; + topAppBarContextProvider.setValue(content.topAppBar); + }), + ); + } + + override render(): unknown { + this._logger.logMethod?.('render'); + return this._menuTemplate(); + } + + protected* _menuTemplate(): unknown { + yield html``; + yield this._boxTemplate(this?.content?.about); + yield this._boxTemplate(this?.content?.product); + yield html``; + yield this._boxTemplate(this?.content?.catalogue); + yield mapIterable(this, this?.content?.socialList, this._boxTemplate, message('loading')); + } + + protected _boxTemplate(box?: BoxType): unknown { + const slot = box?.slot == null ? nothing : unsafeHTML(box.slot); + return html`${slot}`; + } +} diff --git a/uniquely/keep-pwa/src/ui/stuff/app-footer.ts b/uniquely/keep-pwa/src/ui/stuff/app-footer.ts new file mode 100644 index 000000000..65108e473 --- /dev/null +++ b/uniquely/keep-pwa/src/ui/stuff/app-footer.ts @@ -0,0 +1,44 @@ +import {customElement, AlwatrBaseElement, css, html, SignalMixin, LocalizeMixin} from '@alwatr/element'; +import {message} from '@alwatr/i18n'; + +declare global { + interface HTMLElementTagNameMap { + 'alwatr-app-footer': AlwatrPageHome; + } +} + +/** + * Alwatr Keep App Footer + */ +@customElement('alwatr-app-footer') +export class AlwatrPageHome extends LocalizeMixin(SignalMixin(AlwatrBaseElement)) { + static override styles = css` + :host { + display: block; + flex-grow: 0; + flex-shrink: 0; + direction: ltr; + text-align: center; + color: var(--sys-color-on-secondary-container); + padding: calc(2 * var(--sys-spacing-track)) var(--sys-spacing-track) var(--sys-spacing-track); + background-color: var(--sys-color-secondary-container); + } + + .version { + font-size: var(--sys-typescale-label-small-font-size); + line-height: var(--sys-typescale-label-small-line-height); + letter-spacing: var(--sys-typescale-label-small-letter-spacing); + opacity: 0.6; + user-select: none; + -webkit-user-select: none; + } + `; + + override render(): unknown { + this._logger.logMethod?.('render'); + return html` +
${message('app_footer_description')}
+
Keep v${_ALWATR_VERSION_}
+ `; + } +} diff --git a/uniquely/keep-pwa/src/ui/stuff/collaboration-box.ts b/uniquely/keep-pwa/src/ui/stuff/collaboration-box.ts new file mode 100644 index 000000000..6e2e1bb1a --- /dev/null +++ b/uniquely/keep-pwa/src/ui/stuff/collaboration-box.ts @@ -0,0 +1,176 @@ +import { + customElement, + css, + html, + state, + LocalizeMixin, + SignalMixin, + AlwatrBaseElement, + type PropertyValues, +} from '@alwatr/element'; +import {message} from '@alwatr/i18n'; +import '@alwatr/ui-kit/card/icon-box.js'; +import {untilNextFrame, untilEvent, delay} from '@alwatr/util'; + +import './collaboration-form.js'; + +import type {AlwatrCollaborationForm} from './collaboration-form.js'; +import type {AlwatrIconBox, IconBoxContent} from '@alwatr/ui-kit/card/icon-box.js'; + + +declare global { + interface HTMLElementTagNameMap { + 'alwatr-collaboration-box': AlwatrCollaborationBox; + } +} + +/** + * Keep collaboration box element + */ +@customElement('alwatr-collaboration-box') +export class AlwatrCollaborationBox extends LocalizeMixin(SignalMixin(AlwatrBaseElement)) { + static override styles = css` + :host { + display: block; + padding: 0; + } + + .success { + color: var(--sys-color-primary); + font-family: var(--sys-typescale-label-large-font-family-name); + font-weight: var(--sys-typescale-label-large-font-weight); + font-size: var(--sys-typescale-label-large-font-size); + letter-spacing: var(--sys-typescale-label-large-letter-spacing); + line-height: var(--sys-typescale-label-large-line-height); + } + `; + + @state() + expanded = false; + + @state() + submitted = false; + + protected _box: AlwatrIconBox | null = null; + + override render(): unknown { + this._logger.logMethod?.('render'); + return html` + ${this._boxContentTemplate()} + `; + } + + protected get _iconBoxContent(): IconBoxContent { + return { + icon: 'people-outline', + headline: message('collaboration_form_title'), + elevated: 1, + stated: !this.expanded, + highlight: !this.expanded && !this.submitted, + }; + } + + protected override firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + this._box = this.renderRoot.querySelector('alwatr-icon-box'); + } + + protected _boxContentTemplate(): unknown { + if (this.expanded) { + return html``; + } + else if (this.submitted) { + return html`${message('form_submitted')}`; + } + else { + return html`${message('collaboration_form_description')}`; + } + } + + protected async _click(): Promise { + this._logger.logMethod?.('_click'); + if (!this.expanded && !this.submitted) { + await this._currentAnimate; + this._currentAnimate = this._animateExpand(); + } + } + + protected async _formSubmitted(): Promise { + this._logger.logMethod?.('_formSubmitted'); + await this._currentAnimate; + this._currentAnimate = this._animateCollapse(true); + } + + protected async _formCanceled(): Promise { + this._logger.logMethod?.('_formCanceled'); + await this._currentAnimate; + this._currentAnimate = this._animateCollapse(false); + } + + protected _currentAnimate?: Promise; + + protected _collapseHeight = 0; + async _animateExpand(): Promise { + if (this.expanded || this._box == null) return; + this._logger.logMethod?.('_animateExpand'); + const box = this._box; + await untilNextFrame(); + + this._collapseHeight = box.scrollHeight; + box.style.height = this._collapseHeight + 'px'; + this.expanded = true; + await this.updateComplete; + await untilNextFrame(); + + box.style.height = box.scrollHeight + 'px'; + + const form = this.renderRoot.querySelector('#form'); + if (!form) { + this._logger.error('_animateExpand', 'form_not_found'); + this.style.height = 'auto'; + return; + } + form.animateExpand(); + + await untilEvent(box, 'transitionend'); + box.style.height = 'auto'; + } + + async _animateCollapse(submitted: boolean): Promise { + if (!this.expanded || this._box == null) return; + this._logger.logMethod?.('_animateCollapse'); + const box = this._box; + box.style.height = 'auto'; + await untilNextFrame(); + + box.style.height = box.scrollHeight + 'px'; + await untilNextFrame(); + + const form = this.renderRoot.querySelector('#form'); + if (form != null) { + form.animateCollapse(); + await delay(250); + } + + box.style.height = this._collapseHeight + 'px'; + await untilEvent(box, 'transitionend'); + + this.expanded = false; + this.submitted = submitted; + await this.updateComplete; + await box.updateComplete; + await untilNextFrame(); + + if (this._collapseHeight !== box.scrollHeight) { + box.style.height = box.scrollHeight + 'px'; + await untilEvent(box, 'transitionend'); + } + + box.style.height = 'auto'; + } +} diff --git a/uniquely/keep-pwa/src/ui/stuff/collaboration-form.ts b/uniquely/keep-pwa/src/ui/stuff/collaboration-form.ts new file mode 100644 index 000000000..fcd4f48d7 --- /dev/null +++ b/uniquely/keep-pwa/src/ui/stuff/collaboration-form.ts @@ -0,0 +1,171 @@ +import {customElement, css, html, property, LocalizeMixin, SignalMixin, AlwatrBaseElement} from '@alwatr/element'; +import {message} from '@alwatr/i18n'; +import '@alwatr/ui-kit/button/button.js'; +import '@alwatr/ui-kit/radio-group/radio-group.js'; +import '@alwatr/ui-kit/text-field/text-field.js'; + +import {submitFormCommandTrigger} from '../../manager/context.js'; + +import type {RadioGroupOptions} from '@alwatr/ui-kit/radio-group/radio-group.js'; +import type {AlwatrTextField} from '@alwatr/ui-kit/text-field/text-field.js'; + +declare global { + interface HTMLElementTagNameMap { + 'alwatr-collaboration-form': AlwatrCollaborationForm; + } +} + +/** + * Soffit collaboration form element + * + * @attr {Boolean} invisible + */ +@customElement('alwatr-collaboration-form') +export class AlwatrCollaborationForm extends LocalizeMixin(SignalMixin(AlwatrBaseElement)) { + static formId = 'collaboration'; + + get _radioGroupOptions(): RadioGroupOptions { + return { + title: message('activity_field'), + radioGroup: [ + {label: message('tool_distribution'), value: 'tool_distribution'}, + {label: message('sales_tools'), value: 'sales_tools'}, + {label: message('stonemason'), value: 'stonemason'}, + {label: message('contractor'), value: 'contractor'}, + ], + }; + } + + static override styles = css` + :host { + display: block; + transition: opacity var(--sys-motion-duration-medium) var(--sys-motion-easing-normal); + } + + :host([disabled]) { + pointer-events: none; + opacity: var(--sys-surface-disabled-opacity); + } + + alwatr-text-field { + display: block; + margin-top: var(--sys-spacing-track); + } + alwatr-text-field:first-of-type { + margin-top: 0; + } + + :host([invisible]) * { + opacity: 0; + } + + .button-container { + display: flex; + flex-direction: row-reverse; + gap: var(--sys-spacing-track); + margin-top: var(--sys-spacing-track); + } + `; + + @property({type: Boolean, reflect: true}) + disabled = false; + + protected async submit(): Promise { + const bodyJson = this.getFormData(); + this._logger.logMethodArgs?.('submit', bodyJson); + + this.disabled = true; + + const response = await submitFormCommandTrigger.requestWithResponse({ + formId: (this.constructor as typeof AlwatrCollaborationForm).formId, + data: bodyJson, + }); + + if (response) { + this.dispatchEvent(new CustomEvent('form-submitted')); + } + else { + this.disabled = false; + } + } + + protected async cancel(): Promise { + this.dispatchEvent(new CustomEvent('form-canceled')); + } + + protected getFormData(): Record { + this._logger.logMethod?.('getFormData'); + const data: Record = {}; + for (const inputElement of this.renderRoot.querySelectorAll( + 'alwatr-text-field,alwatr-radio-group', + )) { + data[inputElement.name] = inputElement.value as string; + } + return data; + } + + override render(): unknown { + this._logger.logMethod?.('render'); + return [ + this.inputTemplate(), + this.partButtonTemplate(), + ]; + } + + protected inputTemplate(): unknown { + this._logger.logMethod?.('render'); + return html` + + + + + `; + } + + protected partButtonTemplate(): unknown { + return html` +
+ + +
+ `; + } + + async animateExpand(): Promise { + for (const element of this.renderRoot.querySelectorAll('*')) { + element.style.opacity = '1'; + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } + + async animateCollapse(): Promise { + for (const element of [...this.renderRoot.querySelectorAll('*')].reverse()) { + element.style.opacity = '0'; + await new Promise((resolve) => setTimeout(resolve, 40)); + } + } +} diff --git a/uniquely/keep-pwa/tsconfig.json b/uniquely/keep-pwa/tsconfig.json new file mode 100644 index 000000000..e87607747 --- /dev/null +++ b/uniquely/keep-pwa/tsconfig.json @@ -0,0 +1,34 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": ".tsbuildinfo", + // "moduleResolution": "bundler", + "rootDir": "src", + "outDir": "build", + "noEmit": true, + "plugins": [ + { + "name": "ts-lit-plugin", + "strict": true + } + ], + }, + + "include": ["src/**/*.ts", "src/**/*.json"], + "exclude": [], + "references": [ + {"path": "../../core/router"}, + {"path": "../../core/fetch"}, + {"path": "../../core/i18n"}, + {"path": "../../core/type"}, + {"path": "../../core/validator"}, + {"path": "../../core/signal"}, + {"path": "../../core/util"}, + {"path": "../../core/fsm"}, + {"path": "../../core/context"}, + {"path": "../../ui/element"}, + {"path": "../../ui/ui-kit"}, + {"path": "../../ui/pwa-helper"}, + ] +} diff --git a/uniquely/keep-pwa/web-dev-server.config.js b/uniquely/keep-pwa/web-dev-server.config.js new file mode 100644 index 000000000..9c4e13e1e --- /dev/null +++ b/uniquely/keep-pwa/web-dev-server.config.js @@ -0,0 +1,2 @@ +import {config} from '@alwatr/pwa-helper/pwa-dev-server.config.js'; +export default config;