@africanies/angular-web-sdk 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.nx/cache/{13466509747676776487 → 13293303228070520113}/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs +66 -1
- package/.nx/cache/13293303228070520113/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs.map +1 -0
- package/.nx/cache/{13466509747676776487 → 13293303228070520113}/dist/libs/africanies-core/types/africanies-africanies-core.d.ts +6 -1
- package/.nx/cache/{6673686859704879504 → 13387631073062967069}/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs +31 -13
- package/.nx/cache/13387631073062967069/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs.map +1 -0
- package/.nx/cache/{6673686859704879504 → 13387631073062967069}/dist/libs/africanies-theme/tailwind-preset.cjs +85 -17
- package/.nx/cache/{6673686859704879504 → 13387631073062967069}/dist/libs/africanies-theme/types/africanies-africanies-theme.d.ts +7 -3
- package/.nx/cache/16704640000606796352/dist/libs/africanies-storage/README.md +31 -0
- package/.nx/cache/16704640000606796352/dist/libs/africanies-storage/fesm2022/africanies-africanies-storage.mjs +249 -0
- package/.nx/cache/16704640000606796352/dist/libs/africanies-storage/fesm2022/africanies-africanies-storage.mjs.map +1 -0
- package/.nx/cache/16704640000606796352/dist/libs/africanies-storage/package.json +26 -0
- package/.nx/cache/16704640000606796352/dist/libs/africanies-storage/types/africanies-africanies-storage.d.ts +210 -0
- package/.nx/cache/{14876618240301288353 → 2427575433215940812}/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs +2998 -1122
- package/.nx/cache/2427575433215940812/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +1 -0
- package/.nx/cache/{14995846848874973576 → 2427575433215940812}/dist/libs/africanies-ui/types/africanies-africanies-ui.d.ts +450 -140
- package/.nx/cache/{6664915487114632770 → 7730350229628359718}/dist/libs/africanies-icons/assets/icons.sprite.svg +7 -0
- package/.nx/cache/{6664915487114632770 → 7730350229628359718}/dist/libs/africanies-icons/fesm2022/africanies-africanies-icons.mjs +7 -0
- package/.nx/cache/7730350229628359718/dist/libs/africanies-icons/fesm2022/africanies-africanies-icons.mjs.map +1 -0
- package/.nx/cache/{6664915487114632770 → 7730350229628359718}/dist/libs/africanies-icons/types/africanies-africanies-icons.d.ts +2 -2
- package/.nx/cache/{14995846848874973576 → 7821397806208783174}/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs +2572 -962
- package/.nx/cache/7821397806208783174/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +1 -0
- package/.nx/cache/{14876618240301288353 → 7821397806208783174}/dist/libs/africanies-ui/types/africanies-africanies-ui.d.ts +460 -147
- package/.nx/cache/run.json +35 -35
- package/.nx/cache/terminalOutputs/10603154579345867998 +397 -0
- package/.nx/cache/terminalOutputs/12354163057083828162 +80 -0
- package/.nx/cache/terminalOutputs/{9040451482998546630 → 13293303228070520113} +1 -1
- package/.nx/cache/terminalOutputs/{6673686859704879504 → 13387631073062967069} +1 -1
- package/.nx/cache/terminalOutputs/{14061940909886036014 → 14876351543379349919} +46 -43
- package/.nx/cache/terminalOutputs/15336883221450327589 +21 -0
- package/.nx/cache/terminalOutputs/16208231681037145050 +87 -0
- package/.nx/cache/terminalOutputs/{13466509747676776487 → 16704640000606796352} +6 -6
- package/.nx/cache/terminalOutputs/1811006905937373707 +74 -0
- package/.nx/cache/terminalOutputs/2005781115771702035 +8 -0
- package/.nx/cache/terminalOutputs/2360743690449633551 +21 -0
- package/.nx/cache/terminalOutputs/{14876618240301288353 → 2427575433215940812} +2 -2
- package/.nx/cache/terminalOutputs/2981230413857865352 +8 -0
- package/.nx/cache/terminalOutputs/3243550283886864639 +8 -0
- package/.nx/cache/terminalOutputs/3380188914363501109 +231 -0
- package/.nx/cache/terminalOutputs/3699068646900552690 +8 -0
- package/.nx/cache/terminalOutputs/5175669339257931410 +8 -0
- package/.nx/cache/terminalOutputs/6040230831125637867 +8 -0
- package/.nx/cache/terminalOutputs/7730350229628359718 +21 -0
- package/.nx/cache/terminalOutputs/{14995846848874973576 → 7821397806208783174} +2 -2
- package/.nx/cache/terminalOutputs/8098730470212963224 +87 -0
- package/.nx/cache/terminalOutputs/8928690901537686687 +21 -0
- package/.nx/workspace-data/0091ABA9-5C90-580D-953C-8C94E763ADB5-v3.db +0 -0
- package/.nx/workspace-data/0091ABA9-5C90-580D-953C-8C94E763ADB5-v3.db-shm +0 -0
- package/.nx/workspace-data/0091ABA9-5C90-580D-953C-8C94E763ADB5-v3.db-wal +0 -0
- package/.nx/workspace-data/d/daemon.log +4176 -0
- package/.nx/workspace-data/d/server-process.json +2 -2
- package/.nx/workspace-data/file-map.json +2995 -2989
- package/.nx/workspace-data/nx_files.nxt +0 -0
- package/.nx/workspace-data/project-graph.json +2 -2
- package/apps/playground/src/app/pages/forms-page.ts +70 -14
- package/apps/playground/src/app/snippets/forms.snippets.ts +41 -6
- package/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs +60 -0
- package/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs.map +1 -1
- package/dist/libs/africanies-icons/fesm2022/africanies-africanies-icons.mjs +6 -0
- package/dist/libs/africanies-icons/fesm2022/africanies-africanies-icons.mjs.map +1 -1
- package/dist/libs/africanies-storage/fesm2022/africanies-africanies-storage.mjs +6 -0
- package/dist/libs/africanies-storage/fesm2022/africanies-africanies-storage.mjs.map +1 -1
- package/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs +6 -0
- package/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs.map +1 -1
- package/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs +1323 -85
- package/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +1 -1
- package/dist/libs/africanies-ui/types/africanies-africanies-ui.d.ts +201 -15
- package/libs/africanies-core/src/lib/auth/auth-token.service.ts +3 -0
- package/libs/africanies-core/src/lib/auth/auth.service.ts +3 -0
- package/libs/africanies-core/src/lib/country/country.service.ts +3 -0
- package/libs/africanies-core/src/lib/currency/currency.service.ts +3 -0
- package/libs/africanies-core/src/lib/document/document.service.ts +3 -0
- package/libs/africanies-core/src/lib/file/file.service.ts +3 -0
- package/libs/africanies-core/src/lib/filters/filter-options.resolver.ts +3 -0
- package/libs/africanies-core/src/lib/http/api-client.ts +3 -0
- package/libs/africanies-core/src/lib/mode/mode-config.service.ts +3 -0
- package/libs/africanies-core/src/lib/notification/notification.service.ts +3 -0
- package/libs/africanies-core/src/lib/overlay/route-overlay.service.ts +3 -0
- package/libs/africanies-core/src/lib/payment-method/payment-method.service.ts +3 -0
- package/libs/africanies-core/src/lib/plan/plan.service.ts +3 -0
- package/libs/africanies-core/src/lib/product/product.service.ts +3 -0
- package/libs/africanies-core/src/lib/service/service.service.ts +3 -0
- package/libs/africanies-core/src/lib/shipment-method/shipment-method.service.ts +3 -0
- package/libs/africanies-core/src/lib/shipping/shipping-mode.service.ts +3 -0
- package/libs/africanies-core/src/lib/user/user.service.ts +3 -0
- package/libs/africanies-core/src/lib/warehouse/warehouse.service.ts +3 -0
- package/libs/africanies-core/src/lib/zone/zone.service.ts +3 -0
- package/libs/africanies-icons/src/lib/africanies-icon.component.ts +3 -0
- package/libs/africanies-icons/src/lib/icon-registry.service.ts +3 -0
- package/libs/africanies-storage/src/lib/local-storage.service.ts +3 -0
- package/libs/africanies-storage/src/lib/session-storage.service.ts +3 -0
- package/libs/africanies-theme/src/lib/mode-color.service.ts +3 -0
- package/libs/africanies-theme/src/lib/theme.service.ts +3 -0
- package/libs/africanies-ui/src/index.ts +18 -0
- package/libs/africanies-ui/src/lib/accordion/accordion.component.ts +3 -0
- package/libs/africanies-ui/src/lib/action-menu/action-menu-trigger.directive.ts +3 -0
- package/libs/africanies-ui/src/lib/action-menu/action-menu.component.ts +3 -0
- package/libs/africanies-ui/src/lib/alert/alert.component.ts +3 -0
- package/libs/africanies-ui/src/lib/avatar/avatar-menu.component.ts +3 -0
- package/libs/africanies-ui/src/lib/avatar/avatar.component.ts +3 -0
- package/libs/africanies-ui/src/lib/brand/brand-logo.component.ts +3 -0
- package/libs/africanies-ui/src/lib/button/button.component.ts +3 -0
- package/libs/africanies-ui/src/lib/carrier/carrier-logo.component.ts +3 -0
- package/libs/africanies-ui/src/lib/chip/chip.component.ts +3 -0
- package/libs/africanies-ui/src/lib/content-stack/content-stack.component.ts +3 -0
- package/libs/africanies-ui/src/lib/copy-button/copy-button.component.ts +3 -0
- package/libs/africanies-ui/src/lib/feedback/async-state.component.ts +3 -0
- package/libs/africanies-ui/src/lib/feedback/empty-state.component.ts +3 -0
- package/libs/africanies-ui/src/lib/feedback/error-indicator.component.ts +3 -0
- package/libs/africanies-ui/src/lib/feedback/error-state.component.ts +3 -0
- package/libs/africanies-ui/src/lib/feedback/loading-state.component.ts +3 -0
- package/libs/africanies-ui/src/lib/filters/filter-drawer.panel.ts +3 -0
- package/libs/africanies-ui/src/lib/filters/filter-drawer.service.ts +3 -0
- package/libs/africanies-ui/src/lib/filters/filter-query.service.ts +3 -0
- package/libs/africanies-ui/src/lib/forms/address-input/address-input.component.ts +3 -0
- package/libs/africanies-ui/src/lib/forms/address-input/google-places.service.ts +3 -0
- package/libs/africanies-ui/src/lib/forms/checkbox/checkbox.component.ts +3 -0
- package/libs/africanies-ui/src/lib/forms/date-picker/date-picker.component.ts +3 -0
- package/libs/africanies-ui/src/lib/forms/file-upload/camera-capture.dialog.ts +3 -0
- package/libs/africanies-ui/src/lib/forms/file-upload/file-preview.dialog.ts +3 -0
- package/libs/africanies-ui/src/lib/forms/file-upload/file-upload.component.ts +3 -0
- package/libs/africanies-ui/src/lib/forms/index.ts +20 -0
- package/libs/africanies-ui/src/lib/forms/number-input/number-input.component.ts +3 -0
- package/libs/africanies-ui/src/lib/forms/otp-input/otp-input.component.ts +3 -0
- package/libs/africanies-ui/src/lib/forms/phone-input/index.ts +21 -0
- package/libs/africanies-ui/src/lib/forms/phone-input/phone-codes.ts +264 -0
- package/libs/africanies-ui/src/lib/forms/phone-input/phone-input.component.ts +480 -0
- package/libs/africanies-ui/src/lib/forms/phone-input/phone-input.types.ts +35 -0
- package/libs/africanies-ui/src/lib/forms/phone-input/phone-input.utils.spec.ts +81 -0
- package/libs/africanies-ui/src/lib/forms/phone-input/phone-input.utils.ts +275 -0
- package/libs/africanies-ui/src/lib/forms/radio/radio.component.ts +3 -0
- package/libs/africanies-ui/src/lib/forms/search-combobox/search-combobox.component.ts +3 -0
- package/libs/africanies-ui/src/lib/forms/select/select.component.ts +3 -0
- package/libs/africanies-ui/src/lib/forms/text-input/text-input.component.ts +3 -0
- package/libs/africanies-ui/src/lib/forms/textarea/textarea.component.ts +3 -0
- package/libs/africanies-ui/src/lib/forms/toggle/toggle.component.ts +3 -0
- package/libs/africanies-ui/src/lib/image/image.component.ts +3 -0
- package/libs/africanies-ui/src/lib/info-popover/info-popover-content.directive.ts +3 -0
- package/libs/africanies-ui/src/lib/info-popover/info-popover-trigger.directive.ts +3 -0
- package/libs/africanies-ui/src/lib/info-popover/info-popover.component.ts +3 -0
- package/libs/africanies-ui/src/lib/layout/app-shell-content-header.component.ts +3 -0
- package/libs/africanies-ui/src/lib/layout/app-shell-header-slot.directive.ts +3 -0
- package/libs/africanies-ui/src/lib/layout/app-shell-header.component.ts +9 -0
- package/libs/africanies-ui/src/lib/layout/app-shell.component.ts +3 -0
- package/libs/africanies-ui/src/lib/layout/header-weather.spec.ts +62 -94
- package/libs/africanies-ui/src/lib/layout/header-weather.ts +54 -61
- package/libs/africanies-ui/src/lib/layout/page-header.component.ts +3 -0
- package/libs/africanies-ui/src/lib/modules/africanies-actions.module.ts +3 -0
- package/libs/africanies-ui/src/lib/modules/africanies-feedback.module.ts +3 -0
- package/libs/africanies-ui/src/lib/modules/africanies-forms.module.ts +5 -0
- package/libs/africanies-ui/src/lib/modules/africanies-info-popover.module.ts +3 -0
- package/libs/africanies-ui/src/lib/modules/africanies-navigation.module.ts +3 -0
- package/libs/africanies-ui/src/lib/modules/africanies-stepper.module.ts +3 -0
- package/libs/africanies-ui/src/lib/modules/africanies-table.module.ts +3 -0
- package/libs/africanies-ui/src/lib/modules/africanies-tooltip.module.ts +3 -0
- package/libs/africanies-ui/src/lib/navigation/breadcrumb/breadcrumb.component.ts +3 -0
- package/libs/africanies-ui/src/lib/navigation/segment/segment.component.ts +5 -1
- package/libs/africanies-ui/src/lib/navigation/shipping-mode-switch/shipping-mode-switch.component.ts +3 -0
- package/libs/africanies-ui/src/lib/navigation/side-nav/africanies-side-nav.component.ts +3 -0
- package/libs/africanies-ui/src/lib/navigation/tabs/tab-def.directive.ts +3 -0
- package/libs/africanies-ui/src/lib/navigation/tabs/tabs.component.ts +3 -0
- package/libs/africanies-ui/src/lib/notifications/notification-drawer.panel.ts +3 -0
- package/libs/africanies-ui/src/lib/notifications/notification-drawer.service.ts +3 -0
- package/libs/africanies-ui/src/lib/overlay/confirm-dialog.component.ts +3 -0
- package/libs/africanies-ui/src/lib/overlay/confirm.service.ts +3 -0
- package/libs/africanies-ui/src/lib/overlay/drawer.service.ts +3 -0
- package/libs/africanies-ui/src/lib/overlay/modal.service.ts +3 -0
- package/libs/africanies-ui/src/lib/overlay/overlay-frame.component.ts +9 -0
- package/libs/africanies-ui/src/lib/pagination/pagination.component.ts +3 -0
- package/libs/africanies-ui/src/lib/stepper/step-def.directive.ts +3 -0
- package/libs/africanies-ui/src/lib/stepper/stepper.component.ts +3 -0
- package/libs/africanies-ui/src/lib/table/cell-def.directive.ts +3 -0
- package/libs/africanies-ui/src/lib/table/header-cell-def.directive.ts +3 -0
- package/libs/africanies-ui/src/lib/table/row-detail-def.directive.ts +3 -0
- package/libs/africanies-ui/src/lib/table/table.component.ts +3 -0
- package/libs/africanies-ui/src/lib/toast/toast-host.component.ts +4 -1
- package/libs/africanies-ui/src/lib/toast/toast-item.component.ts +3 -0
- package/libs/africanies-ui/src/lib/toast/toast.service.ts +3 -0
- package/libs/africanies-ui/src/lib/tooltip/tooltip-trigger.directive.ts +3 -0
- package/libs/africanies-ui/src/lib/tooltip/tooltip.component.ts +3 -0
- package/package.json +1 -1
- package/.nx/cache/13466509747676776487/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs.map +0 -1
- package/.nx/cache/14416694322692463866/dist/libs/africanies-models/README.md +0 -60
- package/.nx/cache/14416694322692463866/dist/libs/africanies-models/fesm2022/africanies-africanies-models.mjs +0 -599
- package/.nx/cache/14416694322692463866/dist/libs/africanies-models/fesm2022/africanies-africanies-models.mjs.map +0 -1
- package/.nx/cache/14416694322692463866/dist/libs/africanies-models/package.json +0 -23
- package/.nx/cache/14416694322692463866/dist/libs/africanies-models/types/africanies-africanies-models.d.ts +0 -1452
- package/.nx/cache/14876618240301288353/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +0 -1
- package/.nx/cache/14995846848874973576/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +0 -1
- package/.nx/cache/17626862048742598274/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs +0 -221
- package/.nx/cache/17626862048742598274/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs.map +0 -1
- package/.nx/cache/17626862048742598274/dist/libs/africanies-theme/tailwind-preset.cjs +0 -253
- package/.nx/cache/17626862048742598274/dist/libs/africanies-theme/types/africanies-africanies-theme.d.ts +0 -166
- package/.nx/cache/6664915487114632770/dist/libs/africanies-icons/fesm2022/africanies-africanies-icons.mjs.map +0 -1
- package/.nx/cache/6673686859704879504/dist/libs/africanies-theme/README.md +0 -46
- package/.nx/cache/6673686859704879504/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs.map +0 -1
- package/.nx/cache/6673686859704879504/dist/libs/africanies-theme/package.json +0 -35
- package/.nx/cache/9040451482998546630/dist/libs/africanies-core/README.md +0 -85
- package/.nx/cache/9040451482998546630/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs +0 -4511
- package/.nx/cache/9040451482998546630/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs.map +0 -1
- package/.nx/cache/9040451482998546630/dist/libs/africanies-core/package.json +0 -31
- package/.nx/cache/9040451482998546630/dist/libs/africanies-core/types/africanies-africanies-core.d.ts +0 -2741
- package/.nx/cache/terminalOutputs/13555699813138069787 +0 -6
- package/.nx/cache/terminalOutputs/14416694322692463866 +0 -108
- package/.nx/cache/terminalOutputs/1446890025183346611 +0 -32
- package/.nx/cache/terminalOutputs/17394769338046050914 +0 -8
- package/.nx/cache/terminalOutputs/17626862048742598274 +0 -21
- package/.nx/cache/terminalOutputs/6337357866566791975 +0 -6
- package/.nx/cache/terminalOutputs/6664915487114632770 +0 -44
- package/.nx/cache/terminalOutputs/783228502946611950 +0 -19
- package/.nx/cache/terminalOutputs/7978448102466861029 +0 -32
- /package/.nx/cache/{13466509747676776487 → 13293303228070520113}/dist/libs/africanies-core/README.md +0 -0
- /package/.nx/cache/{13466509747676776487 → 13293303228070520113}/dist/libs/africanies-core/package.json +0 -0
- /package/.nx/cache/{17626862048742598274 → 13387631073062967069}/dist/libs/africanies-theme/README.md +0 -0
- /package/.nx/cache/{17626862048742598274 → 13387631073062967069}/dist/libs/africanies-theme/package.json +0 -0
- /package/.nx/cache/{14876618240301288353 → 2427575433215940812}/dist/libs/africanies-ui/README.md +0 -0
- /package/.nx/cache/{14876618240301288353 → 2427575433215940812}/dist/libs/africanies-ui/assets/brand/africanies-logo-mini.png +0 -0
- /package/.nx/cache/{14876618240301288353 → 2427575433215940812}/dist/libs/africanies-ui/assets/brand/africanies-logo.png +0 -0
- /package/.nx/cache/{14876618240301288353 → 2427575433215940812}/dist/libs/africanies-ui/assets/brand/africanies-logo.svg +0 -0
- /package/.nx/cache/{14876618240301288353 → 2427575433215940812}/dist/libs/africanies-ui/assets/carriers/dhl.svg +0 -0
- /package/.nx/cache/{14876618240301288353 → 2427575433215940812}/dist/libs/africanies-ui/package.json +0 -0
- /package/.nx/cache/{6664915487114632770 → 7730350229628359718}/dist/libs/africanies-icons/README.md +0 -0
- /package/.nx/cache/{6664915487114632770 → 7730350229628359718}/dist/libs/africanies-icons/package.json +0 -0
- /package/.nx/cache/{14995846848874973576 → 7821397806208783174}/dist/libs/africanies-ui/README.md +0 -0
- /package/.nx/cache/{14995846848874973576 → 7821397806208783174}/dist/libs/africanies-ui/assets/brand/africanies-logo-mini.png +0 -0
- /package/.nx/cache/{14995846848874973576 → 7821397806208783174}/dist/libs/africanies-ui/assets/brand/africanies-logo.png +0 -0
- /package/.nx/cache/{14995846848874973576 → 7821397806208783174}/dist/libs/africanies-ui/assets/brand/africanies-logo.svg +0 -0
- /package/.nx/cache/{14995846848874973576 → 7821397806208783174}/dist/libs/africanies-ui/assets/carriers/dhl.svg +0 -0
- /package/.nx/cache/{14995846848874973576 → 7821397806208783174}/dist/libs/africanies-ui/package.json +0 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { countryFlagUrl } from '@africanies/africanies-core';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
PHONE_DIAL_CODES,
|
|
5
|
+
PHONE_MAX_NATIONAL_DIGITS,
|
|
6
|
+
PHONE_MIN_NATIONAL_DIGITS,
|
|
7
|
+
} from './phone-codes';
|
|
8
|
+
import type { PhoneCountryOption, PhoneNumberValue } from './phone-input.types';
|
|
9
|
+
|
|
10
|
+
const regionNames =
|
|
11
|
+
typeof Intl !== 'undefined' && 'DisplayNames' in Intl
|
|
12
|
+
? new Intl.DisplayNames(['en'], { type: 'region' })
|
|
13
|
+
: null;
|
|
14
|
+
|
|
15
|
+
/** Digits only from a phone draft. */
|
|
16
|
+
export function phoneDigitsOnly(value: string | null | undefined): string {
|
|
17
|
+
return (value ?? '').replace(/\D/g, '');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Normalize ISO2 to uppercase two letters, or `''`. */
|
|
21
|
+
export function normalizePhoneIso2(iso2: string | null | undefined): string {
|
|
22
|
+
const code = (iso2 ?? '').trim().toUpperCase();
|
|
23
|
+
return /^[A-Z]{2}$/.test(code) ? code : '';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Dial code for an ISO2 (with `+`), or `''` when unknown. */
|
|
27
|
+
export function dialCodeForIso2(iso2: string | null | undefined): string {
|
|
28
|
+
const code = normalizePhoneIso2(iso2);
|
|
29
|
+
return code ? (PHONE_DIAL_CODES[code] ?? '') : '';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** English country name from ISO2 (falls back to the code). */
|
|
33
|
+
export function phoneCountryName(iso2: string): string {
|
|
34
|
+
const code = normalizePhoneIso2(iso2);
|
|
35
|
+
if (!code) {
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return regionNames?.of(code) ?? code;
|
|
40
|
+
} catch {
|
|
41
|
+
return code;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** All dial-code countries for the picker (sorted by name). */
|
|
46
|
+
export function phoneCountryOptions(): PhoneCountryOption[] {
|
|
47
|
+
return Object.keys(PHONE_DIAL_CODES)
|
|
48
|
+
.map((iso2) => {
|
|
49
|
+
const dialCode = PHONE_DIAL_CODES[iso2]!;
|
|
50
|
+
return {
|
|
51
|
+
iso2,
|
|
52
|
+
dialCode,
|
|
53
|
+
name: phoneCountryName(iso2),
|
|
54
|
+
flagUrl: countryFlagUrl(iso2, { width: 40 }),
|
|
55
|
+
};
|
|
56
|
+
})
|
|
57
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Filter dial-code countries by name, ISO2, or dial digits.
|
|
62
|
+
* Digit matching is skipped when the query has no digits — otherwise
|
|
63
|
+
* `''.includes('')` would keep every country for letter-only searches.
|
|
64
|
+
*/
|
|
65
|
+
export function filterPhoneCountries(
|
|
66
|
+
countries: readonly PhoneCountryOption[],
|
|
67
|
+
query: string,
|
|
68
|
+
): PhoneCountryOption[] {
|
|
69
|
+
const q = query.trim().toLowerCase();
|
|
70
|
+
if (!q) {
|
|
71
|
+
return [...countries];
|
|
72
|
+
}
|
|
73
|
+
const qDigits = phoneDigitsOnly(q);
|
|
74
|
+
return countries.filter((row) => {
|
|
75
|
+
if (
|
|
76
|
+
row.name.toLowerCase().includes(q) ||
|
|
77
|
+
row.iso2.toLowerCase().includes(q) ||
|
|
78
|
+
row.dialCode.toLowerCase().includes(q)
|
|
79
|
+
) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
return qDigits.length > 0
|
|
83
|
+
? phoneDigitsOnly(row.dialCode).includes(qDigits)
|
|
84
|
+
: false;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Match an E.164 / international string to the longest known dial code.
|
|
90
|
+
* Prefers `hintIso2` when several countries share the same dial code (e.g. `+1`).
|
|
91
|
+
*/
|
|
92
|
+
export function matchDialCode(
|
|
93
|
+
e164OrInternational: string,
|
|
94
|
+
hintIso2?: string | null,
|
|
95
|
+
): { iso2: string; dialCode: string; nationalDigits: string } | null {
|
|
96
|
+
const digits = phoneDigitsOnly(e164OrInternational);
|
|
97
|
+
if (!digits) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const hint = normalizePhoneIso2(hintIso2);
|
|
102
|
+
if (hint) {
|
|
103
|
+
const hintDial = PHONE_DIAL_CODES[hint];
|
|
104
|
+
if (hintDial) {
|
|
105
|
+
const hintDigits = phoneDigitsOnly(hintDial);
|
|
106
|
+
if (digits.startsWith(hintDigits)) {
|
|
107
|
+
return {
|
|
108
|
+
iso2: hint,
|
|
109
|
+
dialCode: hintDial,
|
|
110
|
+
nationalDigits: digits.slice(hintDigits.length),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let best: { iso2: string; dialCode: string; nationalDigits: string } | null =
|
|
117
|
+
null;
|
|
118
|
+
for (const [iso2, dialCode] of Object.entries(PHONE_DIAL_CODES)) {
|
|
119
|
+
const dialDigits = phoneDigitsOnly(dialCode);
|
|
120
|
+
if (!digits.startsWith(dialDigits)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const nationalDigits = digits.slice(dialDigits.length);
|
|
124
|
+
if (
|
|
125
|
+
!best ||
|
|
126
|
+
dialDigits.length > phoneDigitsOnly(best.dialCode).length
|
|
127
|
+
) {
|
|
128
|
+
best = { iso2, dialCode, nationalDigits };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return best;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Strip a leading dial code from a stored phone (classic `removeDialCode`).
|
|
136
|
+
* Returns national digits for prefilling the input.
|
|
137
|
+
*/
|
|
138
|
+
export function removeDialCode(
|
|
139
|
+
phone: string | null | undefined,
|
|
140
|
+
hintIso2?: string | null,
|
|
141
|
+
): string {
|
|
142
|
+
const raw = (phone ?? '').trim();
|
|
143
|
+
if (!raw) {
|
|
144
|
+
return '';
|
|
145
|
+
}
|
|
146
|
+
const matched = matchDialCode(raw, hintIso2);
|
|
147
|
+
if (matched) {
|
|
148
|
+
return matched.nationalDigits;
|
|
149
|
+
}
|
|
150
|
+
return phoneDigitsOnly(raw);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Group national digits for readable display (… + last 4). */
|
|
154
|
+
export function formatNationalDigits(digits: string): string {
|
|
155
|
+
const clean = phoneDigitsOnly(digits).slice(0, PHONE_MAX_NATIONAL_DIGITS);
|
|
156
|
+
if (!clean) {
|
|
157
|
+
return '';
|
|
158
|
+
}
|
|
159
|
+
if (clean.length <= 4) {
|
|
160
|
+
return clean;
|
|
161
|
+
}
|
|
162
|
+
const end = clean.slice(-4);
|
|
163
|
+
const head = clean.slice(0, -4).replace(/(\d{3})(?=\d)/g, '$1 ').trim();
|
|
164
|
+
return `${head} ${end}`.trim();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Build the structured value (or `null` when empty).
|
|
169
|
+
*/
|
|
170
|
+
export function buildPhoneNumberValue(
|
|
171
|
+
iso2: string,
|
|
172
|
+
nationalDraft: string,
|
|
173
|
+
): PhoneNumberValue | null {
|
|
174
|
+
const countryCode = normalizePhoneIso2(iso2);
|
|
175
|
+
const dialCode = dialCodeForIso2(countryCode);
|
|
176
|
+
if (!countryCode || !dialCode) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const nationalDigits = phoneDigitsOnly(nationalDraft).slice(
|
|
181
|
+
0,
|
|
182
|
+
PHONE_MAX_NATIONAL_DIGITS,
|
|
183
|
+
);
|
|
184
|
+
if (!nationalDigits) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const number = formatNationalDigits(nationalDigits);
|
|
189
|
+
const e164Number = `${dialCode}${nationalDigits}`;
|
|
190
|
+
const internationalNumber = `${dialCode} ${number}`.trim();
|
|
191
|
+
// Trunk-prefix `0` for national display when digits omit it (classic ngx shape).
|
|
192
|
+
const nationalNumber = nationalDigits.startsWith('0')
|
|
193
|
+
? number
|
|
194
|
+
: `0${number}`;
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
number,
|
|
198
|
+
internationalNumber,
|
|
199
|
+
nationalNumber,
|
|
200
|
+
e164Number,
|
|
201
|
+
countryCode,
|
|
202
|
+
dialCode,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Canonical outbound phone string — prefer E.164 from a structured value.
|
|
208
|
+
*/
|
|
209
|
+
export function phoneToE164(
|
|
210
|
+
phone: PhoneNumberValue | string | null | undefined,
|
|
211
|
+
): string {
|
|
212
|
+
if (phone == null) {
|
|
213
|
+
return '';
|
|
214
|
+
}
|
|
215
|
+
if (typeof phone === 'string') {
|
|
216
|
+
const digits = phoneDigitsOnly(phone);
|
|
217
|
+
if (!digits) {
|
|
218
|
+
return '';
|
|
219
|
+
}
|
|
220
|
+
return phone.trim().startsWith('+') ? `+${digits}` : phone.trim();
|
|
221
|
+
}
|
|
222
|
+
return phone.e164Number?.trim() || '';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Soft validity: has dial code + enough national digits (not full libphonenumber).
|
|
227
|
+
*/
|
|
228
|
+
export function isPhoneNumberComplete(
|
|
229
|
+
phone: PhoneNumberValue | null | undefined,
|
|
230
|
+
): boolean {
|
|
231
|
+
if (!phone?.e164Number) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
const national = phoneDigitsOnly(phone.number);
|
|
235
|
+
return (
|
|
236
|
+
national.length >= PHONE_MIN_NATIONAL_DIGITS &&
|
|
237
|
+
national.length <= PHONE_MAX_NATIONAL_DIGITS
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Split an incoming control value (E.164 string or structured) into UI state.
|
|
243
|
+
*/
|
|
244
|
+
export function parsePhoneControlValue(
|
|
245
|
+
value: string | PhoneNumberValue | null | undefined,
|
|
246
|
+
fallbackIso2: string,
|
|
247
|
+
): { iso2: string; nationalDigits: string } {
|
|
248
|
+
const fallback = normalizePhoneIso2(fallbackIso2) || 'US';
|
|
249
|
+
|
|
250
|
+
if (value == null || value === '') {
|
|
251
|
+
return { iso2: fallback, nationalDigits: '' };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (typeof value === 'object') {
|
|
255
|
+
const iso2 = normalizePhoneIso2(value.countryCode) || fallback;
|
|
256
|
+
const national =
|
|
257
|
+
phoneDigitsOnly(value.number) ||
|
|
258
|
+
phoneDigitsOnly(value.nationalNumber) ||
|
|
259
|
+
removeDialCode(value.e164Number, iso2);
|
|
260
|
+
return { iso2, nationalDigits: national };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const matched = matchDialCode(value, fallback);
|
|
264
|
+
if (matched) {
|
|
265
|
+
return {
|
|
266
|
+
iso2: matched.iso2,
|
|
267
|
+
nationalDigits: matched.nationalDigits,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
iso2: fallback,
|
|
273
|
+
nationalDigits: phoneDigitsOnly(value),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
@@ -118,6 +118,9 @@ let nextRadioId = 0;
|
|
|
118
118
|
</fieldset>
|
|
119
119
|
`,
|
|
120
120
|
})
|
|
121
|
+
/**
|
|
122
|
+
* Radio group following the shared AFRICANIES form field pattern.
|
|
123
|
+
*/
|
|
121
124
|
export class RadioComponent<T = string> implements ControlValueAccessor {
|
|
122
125
|
private readonly modeColor = inject(ModeColorService);
|
|
123
126
|
|
|
@@ -269,6 +269,9 @@ const PANEL_POSITIONS: ConnectedPosition[] = [
|
|
|
269
269
|
}
|
|
270
270
|
`,
|
|
271
271
|
})
|
|
272
|
+
/**
|
|
273
|
+
* Generic type-as-you-search combobox with a debounced async lookup and overlay result list.
|
|
274
|
+
*/
|
|
272
275
|
export class SearchComboboxComponent<T> implements ControlValueAccessor {
|
|
273
276
|
private readonly destroyRef = inject(DestroyRef);
|
|
274
277
|
|
|
@@ -443,6 +443,9 @@ const SELECT_PANEL_POSITIONS: ConnectedPosition[] = [
|
|
|
443
443
|
}
|
|
444
444
|
`,
|
|
445
445
|
})
|
|
446
|
+
/**
|
|
447
|
+
* Option select following the shared AFRICANIES form field pattern.
|
|
448
|
+
*/
|
|
446
449
|
export class SelectComponent<T = string> implements ControlValueAccessor {
|
|
447
450
|
private readonly modal = inject(ModalService, { optional: true });
|
|
448
451
|
protected readonly modeColor = inject(ModeColorService);
|
|
@@ -115,6 +115,9 @@ export type TextInputType =
|
|
|
115
115
|
}
|
|
116
116
|
`,
|
|
117
117
|
})
|
|
118
|
+
/**
|
|
119
|
+
* Single-line text field — **reference** form control for `@africanies/africanies-ui`.
|
|
120
|
+
*/
|
|
118
121
|
export class TextInputComponent implements ControlValueAccessor {
|
|
119
122
|
/** Stable ids so label / aria-describedby stay paired across re-renders. */
|
|
120
123
|
protected readonly controlId = `africanies-text-input-${++nextTextInputId}`;
|
|
@@ -82,6 +82,9 @@ let nextTextareaId = 0;
|
|
|
82
82
|
}
|
|
83
83
|
`,
|
|
84
84
|
})
|
|
85
|
+
/**
|
|
86
|
+
* Multi-line text field following the shared form-control pattern (`shared AFRICANIES form field pattern`). Supports prefix/suffix slots, field-level `error`, and {@link ControlValueAccessor}.
|
|
87
|
+
*/
|
|
85
88
|
export class TextareaComponent implements ControlValueAccessor {
|
|
86
89
|
protected readonly controlId = `africanies-textarea-${++nextTextareaId}`;
|
|
87
90
|
protected readonly hintId = `${this.controlId}-hint`;
|
|
@@ -105,6 +105,9 @@ let nextToggleId = 0;
|
|
|
105
105
|
</div>
|
|
106
106
|
`,
|
|
107
107
|
})
|
|
108
|
+
/**
|
|
109
|
+
* Boolean switch following the shared AFRICANIES form field pattern.
|
|
110
|
+
*/
|
|
108
111
|
export class ToggleComponent implements ControlValueAccessor {
|
|
109
112
|
private readonly modeColor = inject(ModeColorService);
|
|
110
113
|
|
|
@@ -95,6 +95,9 @@ export type ImageFit = 'cover' | 'contain';
|
|
|
95
95
|
</div>
|
|
96
96
|
`,
|
|
97
97
|
})
|
|
98
|
+
/**
|
|
99
|
+
* Image with a shaped loading frame and fallback while the asset fetches or if it fails to render.
|
|
100
|
+
*/
|
|
98
101
|
export class ImageComponent {
|
|
99
102
|
private readonly imgRef = viewChild<ElementRef<HTMLImageElement>>('img');
|
|
100
103
|
|
|
@@ -205,6 +205,9 @@ const PLACEMENT_POSITIONS: Record<InfoPopoverPlacement, ConnectedPosition[]> = {
|
|
|
205
205
|
</ng-template>
|
|
206
206
|
`,
|
|
207
207
|
})
|
|
208
|
+
/**
|
|
209
|
+
* Hover / focus / click info panel with a white card, optional title, and fully projected body content (any child component or markup).
|
|
210
|
+
*/
|
|
208
211
|
export class InfoPopoverComponent {
|
|
209
212
|
protected readonly panelId = `africanies-info-popover-${++nextInfoPopoverId}`;
|
|
210
213
|
|
|
@@ -71,6 +71,9 @@ import { PageHeaderComponent } from './page-header.component';
|
|
|
71
71
|
}
|
|
72
72
|
`,
|
|
73
73
|
})
|
|
74
|
+
/**
|
|
75
|
+
* Content chrome above the app-shell body: Back, breadcrumbs, then the page title and optional subtitle.
|
|
76
|
+
*/
|
|
74
77
|
export class AppShellContentHeaderComponent {
|
|
75
78
|
private readonly router = inject(Router);
|
|
76
79
|
|
|
@@ -38,6 +38,9 @@ export type AppShellHeaderDensity = 'mobile' | 'tablet' | 'desktop';
|
|
|
38
38
|
selector: '[africaniesAppShellHeaderStart]',
|
|
39
39
|
standalone: true,
|
|
40
40
|
})
|
|
41
|
+
/**
|
|
42
|
+
* Marks projected content as extra leading chrome in {@link AppShellHeaderComponent}.
|
|
43
|
+
*/
|
|
41
44
|
export class AppShellHeaderStartDirective {}
|
|
42
45
|
|
|
43
46
|
/**
|
|
@@ -47,6 +50,9 @@ export class AppShellHeaderStartDirective {}
|
|
|
47
50
|
selector: '[africaniesAppShellHeaderEnd]',
|
|
48
51
|
standalone: true,
|
|
49
52
|
})
|
|
53
|
+
/**
|
|
54
|
+
* Marks projected content as extra trailing chrome in {@link AppShellHeaderComponent}.
|
|
55
|
+
*/
|
|
50
56
|
export class AppShellHeaderEndDirective {}
|
|
51
57
|
|
|
52
58
|
/**
|
|
@@ -206,6 +212,9 @@ export class AppShellHeaderEndDirective {}
|
|
|
206
212
|
</div>
|
|
207
213
|
`,
|
|
208
214
|
})
|
|
215
|
+
/**
|
|
216
|
+
* Product header bar for {@link AppShellComponent}: a short time-of-day kicker plus the given name, local weather, live clock, notification inbox drawer, and avatar menu. Breadcrumbs and Back belong in the content column via {@link AppShellContentHeaderComponent}.
|
|
217
|
+
*/
|
|
209
218
|
export class AppShellHeaderComponent {
|
|
210
219
|
private readonly notificationsDrawer = inject(NotificationDrawerService);
|
|
211
220
|
private readonly destroyRef = inject(DestroyRef);
|
|
@@ -85,6 +85,9 @@ export type AppShellLayoutPreview = 'mobile' | 'tablet' | 'desktop';
|
|
|
85
85
|
},
|
|
86
86
|
templateUrl: './app-shell.component.html',
|
|
87
87
|
})
|
|
88
|
+
/**
|
|
89
|
+
* Application chrome scaffold: side nav + sticky header + constrained content.
|
|
90
|
+
*/
|
|
88
91
|
export class AppShellComponent {
|
|
89
92
|
protected readonly modeColor = inject(ModeColorService);
|
|
90
93
|
protected readonly logoMiniUrl = inject(AFRICANIES_BRAND_LOGO_MINI_URL);
|
|
@@ -66,18 +66,15 @@ describe('loadHeaderWeather', () => {
|
|
|
66
66
|
).resolves.toBeNull();
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
it('
|
|
69
|
+
it('does not label the device with the IP city', async () => {
|
|
70
70
|
const fetchFn = jest.fn(async (url: string) => {
|
|
71
71
|
if (String(url).includes('geojs')) {
|
|
72
72
|
return jsonResponse({
|
|
73
|
-
latitude: '
|
|
74
|
-
longitude: '
|
|
73
|
+
latitude: '6.45',
|
|
74
|
+
longitude: '3.4',
|
|
75
75
|
city: 'Lagos',
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
|
-
if (String(url).includes('bigdatacloud')) {
|
|
79
|
-
return jsonResponse({ city: 'Abuja', locality: 'Abuja' });
|
|
80
|
-
}
|
|
81
78
|
return jsonResponse({
|
|
82
79
|
current: { weather_code: 61, temperature_2m: 27.4 },
|
|
83
80
|
});
|
|
@@ -88,38 +85,18 @@ describe('loadHeaderWeather', () => {
|
|
|
88
85
|
).resolves.toEqual({
|
|
89
86
|
kind: 'rain',
|
|
90
87
|
temperatureC: 27.4,
|
|
91
|
-
city:
|
|
92
|
-
});
|
|
93
|
-
expect(fetchFn).toHaveBeenCalledTimes(3);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('falls back to the IP city when reverse geocode is empty', async () => {
|
|
97
|
-
const fetchFn = jest.fn(async (url: string) => {
|
|
98
|
-
if (String(url).includes('geojs')) {
|
|
99
|
-
return jsonResponse({ latitude: 1, longitude: 2, city: 'Lagos' });
|
|
100
|
-
}
|
|
101
|
-
if (String(url).includes('bigdatacloud')) {
|
|
102
|
-
return jsonResponse({});
|
|
103
|
-
}
|
|
104
|
-
return jsonResponse({ current: { weather_code: 0, temperature_2m: 30 } });
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
await expect(
|
|
108
|
-
loadHeaderWeather(fetchFn as unknown as typeof fetch),
|
|
109
|
-
).resolves.toEqual({
|
|
110
|
-
kind: 'clear',
|
|
111
|
-
temperatureC: 30,
|
|
112
|
-
city: 'Lagos',
|
|
88
|
+
city: undefined,
|
|
113
89
|
});
|
|
90
|
+
expect(fetchFn).not.toHaveBeenCalledWith(
|
|
91
|
+
expect.stringContaining('geocoding-api'),
|
|
92
|
+
);
|
|
114
93
|
});
|
|
115
94
|
|
|
116
|
-
it('reuses the same-hour session cache', async () => {
|
|
95
|
+
it('reuses the same-hour session cache when the device city was resolved', async () => {
|
|
96
|
+
stubDeviceAt(9.0765, 7.3986);
|
|
117
97
|
const fetchFn = jest.fn(async (url: string) => {
|
|
118
|
-
if (String(url).includes('
|
|
119
|
-
return jsonResponse({
|
|
120
|
-
}
|
|
121
|
-
if (String(url).includes('bigdatacloud')) {
|
|
122
|
-
return jsonResponse({ city: 'Accra' });
|
|
98
|
+
if (String(url).includes('geocoding-api')) {
|
|
99
|
+
return jsonResponse({ results: [{ name: 'Abuja' }] });
|
|
123
100
|
}
|
|
124
101
|
return jsonResponse({ current: { weather_code: 0, temperature_2m: 30 } });
|
|
125
102
|
});
|
|
@@ -127,17 +104,16 @@ describe('loadHeaderWeather', () => {
|
|
|
127
104
|
const first = await loadHeaderWeather(fetchFn as unknown as typeof fetch);
|
|
128
105
|
const second = await loadHeaderWeather(fetchFn as unknown as typeof fetch);
|
|
129
106
|
|
|
130
|
-
expect(first).toEqual(
|
|
131
|
-
expect(
|
|
107
|
+
expect(first).toEqual({ kind: 'clear', temperatureC: 30, city: 'Abuja' });
|
|
108
|
+
expect(second).toEqual(first);
|
|
109
|
+
expect(fetchFn).toHaveBeenCalledTimes(2);
|
|
132
110
|
});
|
|
133
111
|
|
|
134
112
|
it('fetches again when the cached hour no longer matches', async () => {
|
|
113
|
+
stubDeviceAt(9.0765, 7.3986);
|
|
135
114
|
const fetchFn = jest.fn(async (url: string) => {
|
|
136
|
-
if (String(url).includes('
|
|
137
|
-
return jsonResponse({
|
|
138
|
-
}
|
|
139
|
-
if (String(url).includes('bigdatacloud')) {
|
|
140
|
-
return jsonResponse({ city: 'Lagos' });
|
|
115
|
+
if (String(url).includes('geocoding-api')) {
|
|
116
|
+
return jsonResponse({ results: [{ name: 'Abuja' }] });
|
|
141
117
|
}
|
|
142
118
|
return jsonResponse({ current: { weather_code: 0, temperature_2m: 30.2 } });
|
|
143
119
|
});
|
|
@@ -146,7 +122,7 @@ describe('loadHeaderWeather', () => {
|
|
|
146
122
|
const cacheKey = Object.keys(sessionStorage).find((key) =>
|
|
147
123
|
key.startsWith('africanies-header-weather'),
|
|
148
124
|
);
|
|
149
|
-
expect(cacheKey).
|
|
125
|
+
expect(cacheKey).toBe('africanies-header-weather-v8');
|
|
150
126
|
const stored = JSON.parse(sessionStorage.getItem(cacheKey!) ?? '{}') as {
|
|
151
127
|
hour?: string;
|
|
152
128
|
};
|
|
@@ -154,35 +130,15 @@ describe('loadHeaderWeather', () => {
|
|
|
154
130
|
sessionStorage.setItem(cacheKey!, JSON.stringify(stored));
|
|
155
131
|
|
|
156
132
|
await loadHeaderWeather(fetchFn as unknown as typeof fetch);
|
|
157
|
-
expect(fetchFn).toHaveBeenCalledTimes(
|
|
133
|
+
expect(fetchFn).toHaveBeenCalledTimes(4);
|
|
158
134
|
});
|
|
159
135
|
|
|
160
|
-
it('uses
|
|
161
|
-
|
|
162
|
-
configurable: true,
|
|
163
|
-
value: {
|
|
164
|
-
getCurrentPosition: (success: PositionCallback) => {
|
|
165
|
-
success({
|
|
166
|
-
coords: {
|
|
167
|
-
latitude: 9.0765,
|
|
168
|
-
longitude: 7.3986,
|
|
169
|
-
accuracy: 10,
|
|
170
|
-
altitude: null,
|
|
171
|
-
altitudeAccuracy: null,
|
|
172
|
-
heading: null,
|
|
173
|
-
speed: null,
|
|
174
|
-
toJSON: () => ({}),
|
|
175
|
-
},
|
|
176
|
-
timestamp: Date.now(),
|
|
177
|
-
toJSON: () => ({}),
|
|
178
|
-
} as GeolocationPosition);
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
});
|
|
136
|
+
it('uses device coordinates and ignores the IP city', async () => {
|
|
137
|
+
stubDeviceAt(9.0765, 7.3986);
|
|
182
138
|
|
|
183
139
|
const fetchFn = jest.fn(async (url: string) => {
|
|
184
|
-
if (String(url).includes('
|
|
185
|
-
return jsonResponse({
|
|
140
|
+
if (String(url).includes('geocoding-api')) {
|
|
141
|
+
return jsonResponse({ results: [{ name: 'Abuja' }] });
|
|
186
142
|
}
|
|
187
143
|
if (String(url).includes('geojs')) {
|
|
188
144
|
return jsonResponse({
|
|
@@ -201,34 +157,15 @@ describe('loadHeaderWeather', () => {
|
|
|
201
157
|
temperatureC: 31,
|
|
202
158
|
city: 'Abuja',
|
|
203
159
|
});
|
|
160
|
+
expect(fetchFn).not.toHaveBeenCalledWith(expect.stringContaining('geojs'));
|
|
204
161
|
});
|
|
205
162
|
|
|
206
|
-
it('
|
|
207
|
-
|
|
208
|
-
configurable: true,
|
|
209
|
-
value: {
|
|
210
|
-
getCurrentPosition: (success: PositionCallback) => {
|
|
211
|
-
success({
|
|
212
|
-
coords: {
|
|
213
|
-
latitude: 9.0765,
|
|
214
|
-
longitude: 7.3986,
|
|
215
|
-
accuracy: 10,
|
|
216
|
-
altitude: null,
|
|
217
|
-
altitudeAccuracy: null,
|
|
218
|
-
heading: null,
|
|
219
|
-
speed: null,
|
|
220
|
-
toJSON: () => ({}),
|
|
221
|
-
},
|
|
222
|
-
timestamp: Date.now(),
|
|
223
|
-
toJSON: () => ({}),
|
|
224
|
-
} as GeolocationPosition);
|
|
225
|
-
},
|
|
226
|
-
},
|
|
227
|
-
});
|
|
163
|
+
it('omits the city when the device fix has no place name', async () => {
|
|
164
|
+
stubDeviceAt(9.0765, 7.3986);
|
|
228
165
|
|
|
229
166
|
const fetchFn = jest.fn(async (url: string) => {
|
|
230
|
-
if (String(url).includes('
|
|
231
|
-
return jsonResponse({
|
|
167
|
+
if (String(url).includes('geocoding-api')) {
|
|
168
|
+
return jsonResponse({ results: [{ name: '' }] });
|
|
232
169
|
}
|
|
233
170
|
if (String(url).includes('geojs')) {
|
|
234
171
|
return jsonResponse({
|
|
@@ -245,13 +182,14 @@ describe('loadHeaderWeather', () => {
|
|
|
245
182
|
).resolves.toEqual({
|
|
246
183
|
kind: 'clear',
|
|
247
184
|
temperatureC: 31,
|
|
248
|
-
city:
|
|
185
|
+
city: undefined,
|
|
249
186
|
});
|
|
187
|
+
expect(fetchFn).not.toHaveBeenCalledWith(expect.stringContaining('geojs'));
|
|
250
188
|
});
|
|
251
189
|
|
|
252
190
|
it('refetches when the hour cache has weather but no city', async () => {
|
|
253
191
|
sessionStorage.setItem(
|
|
254
|
-
'africanies-header-weather-
|
|
192
|
+
'africanies-header-weather-v8',
|
|
255
193
|
JSON.stringify({
|
|
256
194
|
hour: `${new Date().getFullYear()}-${new Date().getMonth()}-${new Date().getDate()}-${new Date().getHours()}`,
|
|
257
195
|
kind: 'cloudy',
|
|
@@ -274,7 +212,7 @@ describe('loadHeaderWeather', () => {
|
|
|
274
212
|
).resolves.toEqual({
|
|
275
213
|
kind: 'cloudy',
|
|
276
214
|
temperatureC: 25,
|
|
277
|
-
city:
|
|
215
|
+
city: undefined,
|
|
278
216
|
});
|
|
279
217
|
expect(fetchFn).toHaveBeenCalled();
|
|
280
218
|
});
|
|
@@ -304,6 +242,36 @@ describe('headerWeatherIcon', () => {
|
|
|
304
242
|
});
|
|
305
243
|
});
|
|
306
244
|
|
|
245
|
+
function stubDeviceAt(latitude: number, longitude: number): void {
|
|
246
|
+
Object.defineProperty(navigator, 'permissions', {
|
|
247
|
+
configurable: true,
|
|
248
|
+
value: {
|
|
249
|
+
query: async () => ({ state: 'granted' }),
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
Object.defineProperty(navigator, 'geolocation', {
|
|
253
|
+
configurable: true,
|
|
254
|
+
value: {
|
|
255
|
+
getCurrentPosition: (success: PositionCallback) => {
|
|
256
|
+
success({
|
|
257
|
+
coords: {
|
|
258
|
+
latitude,
|
|
259
|
+
longitude,
|
|
260
|
+
accuracy: 10,
|
|
261
|
+
altitude: null,
|
|
262
|
+
altitudeAccuracy: null,
|
|
263
|
+
heading: null,
|
|
264
|
+
speed: null,
|
|
265
|
+
toJSON: () => ({}),
|
|
266
|
+
},
|
|
267
|
+
timestamp: Date.now(),
|
|
268
|
+
toJSON: () => ({}),
|
|
269
|
+
} as GeolocationPosition);
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
307
275
|
function jsonResponse(body: unknown): Pick<Response, 'ok' | 'json'> {
|
|
308
276
|
return {
|
|
309
277
|
ok: true,
|