Repository: evershopcommerce/evershop Branch: dev Commit: c4a8ff42a1d3 Files: 1781 Total size: 3.7 MB Directory structure: gitextract_6d_loyql/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── pull_request_template.md │ └── workflows/ │ └── build_test.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── changelog.md ├── docker-compose.yml ├── eslint.config.js ├── jest.config.js ├── package.json ├── packages/ │ ├── create-evershop-app/ │ │ ├── README.md │ │ ├── createEverShopApp.js │ │ ├── index.js │ │ ├── package.json │ │ └── sample/ │ │ └── themes/ │ │ └── sample/ │ │ ├── dist/ │ │ │ └── pages/ │ │ │ ├── all/ │ │ │ │ ├── EveryWhere.d.ts │ │ │ │ ├── EveryWhere.js │ │ │ │ └── EveryWhere.js.map │ │ │ └── homepage/ │ │ │ ├── OnlyHomePage.d.ts │ │ │ ├── OnlyHomePage.js │ │ │ └── OnlyHomePage.js.map │ │ ├── package.json │ │ ├── src/ │ │ │ └── pages/ │ │ │ ├── all/ │ │ │ │ └── EveryWhere.tsx │ │ │ └── homepage/ │ │ │ └── OnlyHomePage.tsx │ │ └── tsconfig.json │ ├── evershop/ │ │ ├── .swcrc │ │ ├── README.md │ │ ├── package.json │ │ ├── scripts/ │ │ │ ├── postpack.js │ │ │ ├── postpublish.js │ │ │ └── prepublish.js │ │ ├── src/ │ │ │ ├── bin/ │ │ │ │ ├── build/ │ │ │ │ │ ├── client/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── complie.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── initEnvBuild.ts │ │ │ │ │ └── server/ │ │ │ │ │ ├── index.js │ │ │ │ │ ├── useDDL.js │ │ │ │ │ └── useVendorChunk.js │ │ │ │ ├── dev/ │ │ │ │ │ ├── compileTs.js │ │ │ │ │ ├── enableWatcher.js │ │ │ │ │ ├── hooks.js │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── init.ts │ │ │ │ │ ├── initEnvDev.ts │ │ │ │ │ └── register.js │ │ │ │ ├── evershop.js │ │ │ │ ├── extension/ │ │ │ │ │ └── index.ts │ │ │ │ ├── install/ │ │ │ │ │ ├── createMigrationTable.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── templates/ │ │ │ │ │ └── config.json │ │ │ │ ├── lib/ │ │ │ │ │ ├── addDefaultMiddlewareFuncs.ts │ │ │ │ │ ├── app.js │ │ │ │ │ ├── bootstrap/ │ │ │ │ │ │ ├── bootstrap.ts │ │ │ │ │ │ └── migrate.js │ │ │ │ │ ├── buildEntry.js │ │ │ │ │ ├── devEnvHelper.ts │ │ │ │ │ ├── loadModules.js │ │ │ │ │ ├── normalizePort.js │ │ │ │ │ ├── onError.js │ │ │ │ │ ├── onListening.js │ │ │ │ │ ├── prepare.js │ │ │ │ │ ├── startCronProcess.ts │ │ │ │ │ ├── startSubscriberProcess.ts │ │ │ │ │ ├── startUp.js │ │ │ │ │ └── watch/ │ │ │ │ │ ├── broadcast.js │ │ │ │ │ ├── compileSwc.ts │ │ │ │ │ ├── effect.ts │ │ │ │ │ ├── getDistPaths.ts │ │ │ │ │ ├── getRootPaths.ts │ │ │ │ │ ├── getSrcPaths.ts │ │ │ │ │ ├── isDist.js │ │ │ │ │ ├── isRestartRequired.ts │ │ │ │ │ ├── isSrc.js │ │ │ │ │ ├── processors/ │ │ │ │ │ │ ├── addAdminRoute.ts │ │ │ │ │ │ ├── addApiRoute.ts │ │ │ │ │ │ ├── addComponent.ts │ │ │ │ │ │ ├── addFrontStoreRoute.ts │ │ │ │ │ │ ├── addMiddleware.ts │ │ │ │ │ │ ├── deleteARoute.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── removeMiddleware.ts │ │ │ │ │ │ ├── restart.ts │ │ │ │ │ │ ├── restartCronJob.ts │ │ │ │ │ │ ├── restartSubscriber.ts │ │ │ │ │ │ ├── touch.js │ │ │ │ │ │ ├── updateAdminRoute.ts │ │ │ │ │ │ ├── updateApiRoute.ts │ │ │ │ │ │ └── updateFrontStoreRoute.ts │ │ │ │ │ └── watchHandler.ts │ │ │ │ ├── seed/ │ │ │ │ │ ├── data/ │ │ │ │ │ │ ├── attributes.json │ │ │ │ │ │ ├── categories.json │ │ │ │ │ │ ├── collections.json │ │ │ │ │ │ ├── pages.json │ │ │ │ │ │ ├── products.json │ │ │ │ │ │ └── widgets.json │ │ │ │ │ ├── imageDownloader.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── initEnvDev.ts │ │ │ │ │ ├── seedAttributes.ts │ │ │ │ │ ├── seedCategories.ts │ │ │ │ │ ├── seedCollections.ts │ │ │ │ │ ├── seedImages.ts │ │ │ │ │ ├── seedPages.ts │ │ │ │ │ ├── seedProducts.ts │ │ │ │ │ ├── seedWidgets.ts │ │ │ │ │ └── variantGroupHelpers.ts │ │ │ │ ├── start/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── initEnvStart.ts │ │ │ │ ├── theme/ │ │ │ │ │ ├── active.ts │ │ │ │ │ ├── create.ts │ │ │ │ │ └── twizz.ts │ │ │ │ └── user/ │ │ │ │ ├── changePassword.js │ │ │ │ └── create.js │ │ │ ├── components/ │ │ │ │ ├── admin/ │ │ │ │ │ ├── AttributeGroupSelector.tsx │ │ │ │ │ ├── CategorySelector.tsx │ │ │ │ │ ├── CategoryTree.scss │ │ │ │ │ ├── CategoryTree.tsx │ │ │ │ │ ├── CollectionSelector.tsx │ │ │ │ │ ├── FileBrowser.scss │ │ │ │ │ ├── FileBrowser.tsx │ │ │ │ │ ├── FormButtons.tsx │ │ │ │ │ ├── ImageUploader.scss │ │ │ │ │ ├── ImageUploader.tsx │ │ │ │ │ ├── ImageUploaderSkeleton.tsx │ │ │ │ │ ├── NavigationItem.scss │ │ │ │ │ ├── NavigationItem.tsx │ │ │ │ │ ├── NavigationItemGroup.scss │ │ │ │ │ ├── NavigationItemGroup.tsx │ │ │ │ │ ├── PageHeading.scss │ │ │ │ │ ├── PageHeading.tsx │ │ │ │ │ ├── ProductListSkeleton.tsx │ │ │ │ │ ├── ProductSelector.tsx │ │ │ │ │ ├── SettingMenu.tsx │ │ │ │ │ ├── Spinner.jsx │ │ │ │ │ ├── Status.tsx │ │ │ │ │ └── grid/ │ │ │ │ │ ├── GridPagination.tsx │ │ │ │ │ ├── Thumbnail.tsx │ │ │ │ │ └── header/ │ │ │ │ │ ├── Dummy.tsx │ │ │ │ │ └── Sortable.tsx │ │ │ │ ├── common/ │ │ │ │ │ ├── Area.tsx │ │ │ │ │ ├── Editor.scss │ │ │ │ │ ├── Editor.tsx │ │ │ │ │ ├── ExtendableTable.tsx │ │ │ │ │ ├── Image.tsx │ │ │ │ │ ├── Link.tsx │ │ │ │ │ ├── LoadingBar.scss │ │ │ │ │ ├── LoadingBar.tsx │ │ │ │ │ ├── Meta.tsx │ │ │ │ │ ├── Notification.scss │ │ │ │ │ ├── Notification.tsx │ │ │ │ │ ├── ProductNoThumbnail.tsx │ │ │ │ │ ├── RenderIfTrue.tsx │ │ │ │ │ ├── Script.tsx │ │ │ │ │ ├── SimplePagination.tsx │ │ │ │ │ ├── StaticImage.tsx │ │ │ │ │ ├── Title.tsx │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── app.tsx │ │ │ │ │ ├── customer/ │ │ │ │ │ │ └── address/ │ │ │ │ │ │ └── AddressSummary.jsx │ │ │ │ │ ├── form/ │ │ │ │ │ │ ├── CheckboxField.tsx │ │ │ │ │ │ ├── DateField.tsx │ │ │ │ │ │ ├── DateTimeLocalField.tsx │ │ │ │ │ │ ├── Editor.scss │ │ │ │ │ │ ├── Editor.tsx │ │ │ │ │ │ ├── EmailField.tsx │ │ │ │ │ │ ├── FileField.tsx │ │ │ │ │ │ ├── Form.tsx │ │ │ │ │ │ ├── InputField.tsx │ │ │ │ │ │ ├── NumberField.tsx │ │ │ │ │ │ ├── PasswordField.tsx │ │ │ │ │ │ ├── RadioGroupField.tsx │ │ │ │ │ │ ├── RangeField.tsx │ │ │ │ │ │ ├── ReactSelectCreatableField.tsx │ │ │ │ │ │ ├── ReactSelectField.tsx │ │ │ │ │ │ ├── SelectField.tsx │ │ │ │ │ │ ├── TelField.tsx │ │ │ │ │ │ ├── TextareaField.tsx │ │ │ │ │ │ ├── TimeField.tsx │ │ │ │ │ │ ├── ToggleField.tsx │ │ │ │ │ │ ├── Tooltip.tsx │ │ │ │ │ │ ├── UrlField.tsx │ │ │ │ │ │ ├── editor/ │ │ │ │ │ │ │ ├── GetColumnClasses.tsx │ │ │ │ │ │ │ ├── GetRowClasses.tsx │ │ │ │ │ │ │ ├── RawToolWrapper.ts │ │ │ │ │ │ │ └── RowTemplates.tsx │ │ │ │ │ │ └── utils/ │ │ │ │ │ │ └── getNestedError.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── locale/ │ │ │ │ │ │ ├── CountryOption.jsx │ │ │ │ │ │ ├── CurrencyOption.jsx │ │ │ │ │ │ ├── LanguageOption.jsx │ │ │ │ │ │ ├── ProvinceOption.jsx │ │ │ │ │ │ └── TimezoneOption.jsx │ │ │ │ │ ├── modal/ │ │ │ │ │ │ ├── Alert.jsx │ │ │ │ │ │ └── Alert.scss │ │ │ │ │ ├── react/ │ │ │ │ │ │ ├── Head.jsx │ │ │ │ │ │ ├── client/ │ │ │ │ │ │ │ ├── Client.tsx │ │ │ │ │ │ │ ├── HotReload.tsx │ │ │ │ │ │ │ ├── Hydrate.tsx │ │ │ │ │ │ │ ├── HydrateAdmin.tsx │ │ │ │ │ │ │ ├── HydrateFrontStore.tsx │ │ │ │ │ │ │ └── Index.jsx │ │ │ │ │ │ ├── getComponents.js │ │ │ │ │ │ └── server/ │ │ │ │ │ │ ├── Server.tsx │ │ │ │ │ │ └── render.tsx │ │ │ │ │ └── ui/ │ │ │ │ │ ├── Accordion.tsx │ │ │ │ │ ├── Alert.tsx │ │ │ │ │ ├── AlertDialog.tsx │ │ │ │ │ ├── AspectRatio.tsx │ │ │ │ │ ├── Avatar.tsx │ │ │ │ │ ├── Badge.tsx │ │ │ │ │ ├── Breadcrumb.tsx │ │ │ │ │ ├── Button.tsx │ │ │ │ │ ├── ButtonGroup.tsx │ │ │ │ │ ├── Card.tsx │ │ │ │ │ ├── Chart.tsx │ │ │ │ │ ├── Checkbox.tsx │ │ │ │ │ ├── Circle.tsx │ │ │ │ │ ├── Collapsible.tsx │ │ │ │ │ ├── ContextMenu.tsx │ │ │ │ │ ├── Dialog.tsx │ │ │ │ │ ├── DropdownMenu.tsx │ │ │ │ │ ├── Empty.tsx │ │ │ │ │ ├── Field.tsx │ │ │ │ │ ├── HoverCard.tsx │ │ │ │ │ ├── Input.tsx │ │ │ │ │ ├── InputGroup.tsx │ │ │ │ │ ├── Item.tsx │ │ │ │ │ ├── Kbd.tsx │ │ │ │ │ ├── Label.tsx │ │ │ │ │ ├── Menubar.tsx │ │ │ │ │ ├── NavigationMenu.tsx │ │ │ │ │ ├── Pagination.tsx │ │ │ │ │ ├── Popover.tsx │ │ │ │ │ ├── Progress.tsx │ │ │ │ │ ├── RadioGroup.tsx │ │ │ │ │ ├── ScrollArea.tsx │ │ │ │ │ ├── Select.tsx │ │ │ │ │ ├── Separator.tsx │ │ │ │ │ ├── Sheet.tsx │ │ │ │ │ ├── Sidebar.tsx │ │ │ │ │ ├── Skeleton.tsx │ │ │ │ │ ├── Slider.tsx │ │ │ │ │ ├── Spinner.tsx │ │ │ │ │ ├── Switch.tsx │ │ │ │ │ ├── Table.tsx │ │ │ │ │ ├── Tabs.tsx │ │ │ │ │ ├── Textarea.tsx │ │ │ │ │ ├── Toggle.tsx │ │ │ │ │ ├── ToggleGroup.tsx │ │ │ │ │ ├── Tooltip.tsx │ │ │ │ │ └── hooks/ │ │ │ │ │ └── useIsMobile.tsx │ │ │ │ └── frontStore/ │ │ │ │ ├── Coupon.tsx │ │ │ │ ├── CouponForm.tsx │ │ │ │ ├── Footer.tsx │ │ │ │ ├── Header.scss │ │ │ │ ├── Header.tsx │ │ │ │ ├── Og.tsx │ │ │ │ ├── Pagination.tsx │ │ │ │ ├── cart/ │ │ │ │ │ ├── AddToCart.tsx │ │ │ │ │ ├── CartContext.tsx │ │ │ │ │ ├── CartItems.tsx │ │ │ │ │ ├── CartSummaryItems.tsx │ │ │ │ │ ├── CartTotalSummary.tsx │ │ │ │ │ ├── DefaultCartItemList.tsx │ │ │ │ │ ├── DefaultMiniCartDropdown.tsx │ │ │ │ │ ├── DefaultMiniCartDropdownEmpty.tsx │ │ │ │ │ ├── DefaultMiniCartIcon.tsx │ │ │ │ │ ├── DefaultMiniCartItemList.tsx │ │ │ │ │ ├── DefaultMinicartDropdownSummary.tsx │ │ │ │ │ ├── ItemQuantity.tsx │ │ │ │ │ ├── MiniCart.tsx │ │ │ │ │ └── ShoppingCartEmpty.tsx │ │ │ │ ├── catalog/ │ │ │ │ │ ├── CategoryContext.tsx │ │ │ │ │ ├── CategoryInfo.tsx │ │ │ │ │ ├── CategoryProducts.tsx │ │ │ │ │ ├── CategoryProductsFilter.tsx │ │ │ │ │ ├── CategoryProductsPagination.tsx │ │ │ │ │ ├── DefaultAttributeFilterRender.tsx │ │ │ │ │ ├── DefaultCategoryFilterRender.tsx │ │ │ │ │ ├── DefaultFilterWrapperRender.tsx │ │ │ │ │ ├── DefaultPriceFilterRender.tsx │ │ │ │ │ ├── DefaultProductFilterRender.tsx │ │ │ │ │ ├── DefaultProductFilterSummary.tsx │ │ │ │ │ ├── DefaultVariantSelectorRender.tsx │ │ │ │ │ ├── Media.scss │ │ │ │ │ ├── Media.tsx │ │ │ │ │ ├── ProductContext.tsx │ │ │ │ │ ├── ProductFilter.tsx │ │ │ │ │ ├── ProductList.tsx │ │ │ │ │ ├── ProductListEmptyRender.tsx │ │ │ │ │ ├── ProductListItemRender.tsx │ │ │ │ │ ├── ProductListLoadingSkeleton.tsx │ │ │ │ │ ├── ProductSingleAttributes.tsx │ │ │ │ │ ├── ProductSingleDescription.tsx │ │ │ │ │ ├── ProductSingleForm.tsx │ │ │ │ │ ├── ProductSingleName.tsx │ │ │ │ │ ├── ProductSingleSku.tsx │ │ │ │ │ ├── ProductSorting.tsx │ │ │ │ │ ├── SearchBox.tsx │ │ │ │ │ ├── SearchContext.tsx │ │ │ │ │ ├── SearchInfo.tsx │ │ │ │ │ ├── SearchProducts.tsx │ │ │ │ │ ├── SearchProductsPagination.tsx │ │ │ │ │ └── VariantSelector.tsx │ │ │ │ ├── checkout/ │ │ │ │ │ ├── CheckoutButton.tsx │ │ │ │ │ ├── CheckoutContext.tsx │ │ │ │ │ ├── ContactInformation.tsx │ │ │ │ │ ├── OrderSummaryItems.tsx │ │ │ │ │ ├── OrderTotalSummary.tsx │ │ │ │ │ ├── Payment.tsx │ │ │ │ │ ├── Shipment.tsx │ │ │ │ │ ├── payment/ │ │ │ │ │ │ ├── BillingAddress.tsx │ │ │ │ │ │ └── PaymentMethods.tsx │ │ │ │ │ └── shipment/ │ │ │ │ │ └── ShippingMethods.tsx │ │ │ │ └── customer/ │ │ │ │ ├── AccountInfo.tsx │ │ │ │ ├── CustomerContext.tsx │ │ │ │ ├── LoginForm.tsx │ │ │ │ ├── MyAddresses.tsx │ │ │ │ ├── OrderHistory.tsx │ │ │ │ ├── RegistrationForm.tsx │ │ │ │ ├── ResetPasswordForm.tsx │ │ │ │ └── address/ │ │ │ │ └── addressForm/ │ │ │ │ ├── AddressForm.tsx │ │ │ │ ├── AddressFormLoadingSkeleton.scss │ │ │ │ ├── AddressFormLoadingSkeleton.tsx │ │ │ │ ├── Index.tsx │ │ │ │ ├── NameAndTelephone.tsx │ │ │ │ └── ProvinceAndPostcode.tsx │ │ │ ├── lib/ │ │ │ │ ├── babel/ │ │ │ │ │ ├── config.js │ │ │ │ │ └── index.js │ │ │ │ ├── componee/ │ │ │ │ │ ├── getComponentsByRoute.ts │ │ │ │ │ ├── scanForComponents.ts │ │ │ │ │ ├── scanForRootComponents.ts │ │ │ │ │ └── tests/ │ │ │ │ │ └── unit/ │ │ │ │ │ ├── __mocks__/ │ │ │ │ │ │ ├── modules/ │ │ │ │ │ │ │ ├── firstModule/ │ │ │ │ │ │ │ │ └── pages/ │ │ │ │ │ │ │ │ └── frontStore/ │ │ │ │ │ │ │ │ ├── all/ │ │ │ │ │ │ │ │ │ └── Menu.js │ │ │ │ │ │ │ │ └── productView/ │ │ │ │ │ │ │ │ ├── Name.js │ │ │ │ │ │ │ │ └── Price.js │ │ │ │ │ │ │ └── secondModule/ │ │ │ │ │ │ │ └── pages/ │ │ │ │ │ │ │ └── frontStore/ │ │ │ │ │ │ │ ├── all/ │ │ │ │ │ │ │ │ └── Banner.js │ │ │ │ │ │ │ └── productView/ │ │ │ │ │ │ │ ├── Description.js │ │ │ │ │ │ │ ├── Inventory.js │ │ │ │ │ │ │ └── Name.js │ │ │ │ │ │ └── themes/ │ │ │ │ │ │ └── justatheme/ │ │ │ │ │ │ └── pages/ │ │ │ │ │ │ ├── all/ │ │ │ │ │ │ │ ├── CommentList.js │ │ │ │ │ │ │ └── Shipping.js │ │ │ │ │ │ └── productView/ │ │ │ │ │ │ ├── Name.js │ │ │ │ │ │ ├── OutOfStock.js │ │ │ │ │ │ └── Price.js │ │ │ │ │ └── scanRouteComponents.test.js │ │ │ │ ├── cronjob/ │ │ │ │ │ ├── cronjob.ts │ │ │ │ │ ├── jobManager.ts │ │ │ │ │ └── tests/ │ │ │ │ │ └── unit/ │ │ │ │ │ └── jobManager.test.js │ │ │ │ ├── event/ │ │ │ │ │ ├── callSubscibers.js │ │ │ │ │ ├── emitter.ts │ │ │ │ │ ├── event-manager.js │ │ │ │ │ ├── loadSubscribers.js │ │ │ │ │ └── subscriber.ts │ │ │ │ ├── helpers.ts │ │ │ │ ├── locale/ │ │ │ │ │ ├── countries.ts │ │ │ │ │ ├── currencies.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── provinces.ts │ │ │ │ │ ├── timezones.ts │ │ │ │ │ └── translate/ │ │ │ │ │ ├── _.ts │ │ │ │ │ └── translate.ts │ │ │ │ ├── log/ │ │ │ │ │ ├── CustomColorize.js │ │ │ │ │ └── logger.js │ │ │ │ ├── mail/ │ │ │ │ │ └── emailHelper.ts │ │ │ │ ├── middleware/ │ │ │ │ │ ├── Handler.js │ │ │ │ │ ├── addMiddleware.js │ │ │ │ │ ├── buildMiddlewareFunction.js │ │ │ │ │ ├── delegate.ts │ │ │ │ │ ├── eNext.js │ │ │ │ │ ├── findDublicatedMiddleware.js │ │ │ │ │ ├── getRouteFromPath.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── isErrorHandlerTriggered.js │ │ │ │ │ ├── isNextRequired.js │ │ │ │ │ ├── noDuplicateId.js │ │ │ │ │ ├── parseFromFile.js │ │ │ │ │ ├── scanForMiddlewareFunctions.js │ │ │ │ │ ├── sort.js │ │ │ │ │ └── tests/ │ │ │ │ │ ├── app/ │ │ │ │ │ │ ├── app.js │ │ │ │ │ │ └── modules/ │ │ │ │ │ │ ├── 404page/ │ │ │ │ │ │ │ └── pages/ │ │ │ │ │ │ │ └── frontStore/ │ │ │ │ │ │ │ └── product/ │ │ │ │ │ │ │ ├── [loadProduct]loadCategory.js │ │ │ │ │ │ │ ├── [loadProduct]loadProductImage.js │ │ │ │ │ │ │ ├── loadProduct.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── api/ │ │ │ │ │ │ │ └── api/ │ │ │ │ │ │ │ ├── createA/ │ │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ │ └── global/ │ │ │ │ │ │ │ └── apiGlobal.js │ │ │ │ │ │ ├── authcopy/ │ │ │ │ │ │ │ ├── api/ │ │ │ │ │ │ │ │ ├── createA/ │ │ │ │ │ │ │ │ │ └── [index]afterIndex.js │ │ │ │ │ │ │ │ └── global/ │ │ │ │ │ │ │ │ ├── [context]auth.js │ │ │ │ │ │ │ │ └── apiAuthGlobal.js │ │ │ │ │ │ │ └── pages/ │ │ │ │ │ │ │ └── global/ │ │ │ │ │ │ │ └── [context]auth.js │ │ │ │ │ │ ├── basecopy/ │ │ │ │ │ │ │ ├── api/ │ │ │ │ │ │ │ │ └── global/ │ │ │ │ │ │ │ │ ├── [apiResponse]apiErrorHandler.js │ │ │ │ │ │ │ │ ├── [auth]apiResponse[apiErrorHandler].js │ │ │ │ │ │ │ │ ├── [auth]payloadValidate.js │ │ │ │ │ │ │ │ ├── [payloadValidate]escapeHtml.js │ │ │ │ │ │ │ │ └── context.js │ │ │ │ │ │ │ └── pages/ │ │ │ │ │ │ │ ├── admin/ │ │ │ │ │ │ │ │ ├── adminStaticAsset/ │ │ │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ │ │ └── staticAssets.js │ │ │ │ │ │ │ │ └── all/ │ │ │ │ │ │ │ │ └── adminTitle.js │ │ │ │ │ │ │ ├── frontStore/ │ │ │ │ │ │ │ │ ├── all/ │ │ │ │ │ │ │ │ │ └── title.js │ │ │ │ │ │ │ │ ├── notFound/ │ │ │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ │ │ └── staticAsset/ │ │ │ │ │ │ │ │ ├── [context]staticAssets[auth].js │ │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ │ └── global/ │ │ │ │ │ │ │ ├── [auth]notFound[response].js │ │ │ │ │ │ │ ├── [notFound]dummy[response].js │ │ │ │ │ │ │ ├── [response]errorHandler.js │ │ │ │ │ │ │ ├── context.js │ │ │ │ │ │ │ └── response[errorHandler].js │ │ │ │ │ │ ├── delegate/ │ │ │ │ │ │ │ └── pages/ │ │ │ │ │ │ │ └── frontStore/ │ │ │ │ │ │ │ └── delegateTest/ │ │ │ │ │ │ │ ├── asyncWithNext[collection].js │ │ │ │ │ │ │ ├── async[collection].js │ │ │ │ │ │ │ ├── collection.js │ │ │ │ │ │ │ ├── returnOne[returnTwo].js │ │ │ │ │ │ │ ├── returnThree[collection].js │ │ │ │ │ │ │ ├── returnTwo[returnThree].js │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ ├── syncOne.js │ │ │ │ │ │ │ ├── syncWithNext[collection].js │ │ │ │ │ │ │ └── sync[collection].js │ │ │ │ │ │ ├── error/ │ │ │ │ │ │ │ └── pages/ │ │ │ │ │ │ │ └── frontStore/ │ │ │ │ │ │ │ └── errorHandlerTest/ │ │ │ │ │ │ │ ├── errorInAsync.js │ │ │ │ │ │ │ ├── errorInAsyncWithNext.js │ │ │ │ │ │ │ ├── errorInSync.js │ │ │ │ │ │ │ ├── errorInSyncWithNext.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── graphqlcopy/ │ │ │ │ │ │ │ └── pages/ │ │ │ │ │ │ │ └── global/ │ │ │ │ │ │ │ ├── [bodyParser]buildQuery[graphql].js │ │ │ │ │ │ │ ├── [buildQuery]graphql[notFound].js │ │ │ │ │ │ │ └── bodyParser[buildQuery].js │ │ │ │ │ │ └── handler/ │ │ │ │ │ │ └── pages/ │ │ │ │ │ │ ├── admin/ │ │ │ │ │ │ │ └── productEdit/ │ │ │ │ │ │ │ ├── [loadProduct]loadCategory.js │ │ │ │ │ │ │ ├── [loadProduct]loadProductImage.js │ │ │ │ │ │ │ ├── loadProduct.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ └── frontStore/ │ │ │ │ │ │ └── middleware/ │ │ │ │ │ │ ├── [loadAttribute]loadOptions.js │ │ │ │ │ │ ├── [loadProductImage]loadAttribute.js │ │ │ │ │ │ ├── [loadProduct]loadCategory.js │ │ │ │ │ │ ├── [loadProduct]loadProductImage.js │ │ │ │ │ │ ├── [syncOne,asyncOne]checkExecutionOrderAsync[loadAttribute].js │ │ │ │ │ │ ├── [syncOne,asyncOne]checkExecutionOrder[loadAttribute].js │ │ │ │ │ │ ├── asyncOne[loadAttribute].js │ │ │ │ │ │ ├── loadProduct[loadAttribute].js │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ └── syncOne[loadAttribute].js │ │ │ │ │ └── unit/ │ │ │ │ │ ├── 404page.handling.test.js │ │ │ │ │ ├── 500error.handling.test.js │ │ │ │ │ ├── apiHandler.middleware.test.js │ │ │ │ │ ├── delegate.test.js │ │ │ │ │ ├── handler.getMiddlewaresByRoute.test.js │ │ │ │ │ ├── handlers.middleware.test.js │ │ │ │ │ ├── middleware.buildMiddlewareFunction.test.js │ │ │ │ │ ├── middleware.getRouteFromPath.test.js │ │ │ │ │ ├── middleware.noDublicateId.test.js │ │ │ │ │ └── middleware.scanForMiddlewareFunctions.test.js │ │ │ │ ├── middlewares/ │ │ │ │ │ ├── bodyJson.ts │ │ │ │ │ ├── multerNone.ts │ │ │ │ │ ├── publicStatic.ts │ │ │ │ │ ├── static.ts │ │ │ │ │ └── themePublicStatic.ts │ │ │ │ ├── pathToRegexp.js │ │ │ │ ├── postgres/ │ │ │ │ │ └── connection.ts │ │ │ │ ├── response/ │ │ │ │ │ └── render.ts │ │ │ │ ├── router/ │ │ │ │ │ ├── Router.js │ │ │ │ │ ├── buildAbsoluteUrl.ts │ │ │ │ │ ├── buildUrl.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── loadModuleRoutes.js │ │ │ │ │ ├── registerAdminRoute.js │ │ │ │ │ ├── registerFrontStoreRoute.js │ │ │ │ │ ├── scanForRoutes.js │ │ │ │ │ ├── sortRoutes.js │ │ │ │ │ ├── tests/ │ │ │ │ │ │ └── unit/ │ │ │ │ │ │ ├── a/ │ │ │ │ │ │ │ ├── invalidMethod/ │ │ │ │ │ │ │ │ └── routeOne/ │ │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ │ └── invalidPath/ │ │ │ │ │ │ │ └── routeTwo/ │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── b/ │ │ │ │ │ │ │ ├── routeOne/ │ │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ │ ├── routeThree/ │ │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ │ └── routeTwo/ │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── unit.scanForRoutes.test.js │ │ │ │ │ │ └── unit.validateRoute.test.js │ │ │ │ │ └── validateRoute.js │ │ │ │ ├── util/ │ │ │ │ │ ├── assign.js │ │ │ │ │ ├── buildFilterFromUrl.ts │ │ │ │ │ ├── camelCase.ts │ │ │ │ │ ├── cn.ts │ │ │ │ │ ├── defaultPaginationFilters.js │ │ │ │ │ ├── events.ts │ │ │ │ │ ├── filterOperationMap.ts │ │ │ │ │ ├── formToJson.js │ │ │ │ │ ├── get.ts │ │ │ │ │ ├── getBaseUrl.ts │ │ │ │ │ ├── getConfig.ts │ │ │ │ │ ├── getEnabledTheme.ts │ │ │ │ │ ├── getEnv.ts │ │ │ │ │ ├── hookable.ts │ │ │ │ │ ├── httpStatus.ts │ │ │ │ │ ├── isAjax.ts │ │ │ │ │ ├── isDevelopmentMode.ts │ │ │ │ │ ├── isPlainObject.ts │ │ │ │ │ ├── isProductionMode.ts │ │ │ │ │ ├── isResolvable.ts │ │ │ │ │ ├── jsonParse.ts │ │ │ │ │ ├── jwt.ts │ │ │ │ │ ├── keyGenerator.ts │ │ │ │ │ ├── merge.js │ │ │ │ │ ├── parseImageSizes.ts │ │ │ │ │ ├── passwordHelper.ts │ │ │ │ │ ├── preloadScan.ts │ │ │ │ │ ├── readCsvFile.ts │ │ │ │ │ ├── registry.ts │ │ │ │ │ ├── sanitizeHtml.ts │ │ │ │ │ ├── tests/ │ │ │ │ │ │ └── unit/ │ │ │ │ │ │ ├── util.assign.test.js │ │ │ │ │ │ ├── util.get.test.js │ │ │ │ │ │ ├── util.getConfig.test.js │ │ │ │ │ │ ├── util.hookable.test.js │ │ │ │ │ │ ├── util.jwt.test.js │ │ │ │ │ │ ├── util.merge.test.js │ │ │ │ │ │ ├── util.parseImageSizes.test.js │ │ │ │ │ │ ├── util.preloadScan.test.js │ │ │ │ │ │ └── util.registry.test.js │ │ │ │ │ ├── validateConfiguration.js │ │ │ │ │ └── validator.ts │ │ │ │ ├── webpack/ │ │ │ │ │ ├── createBaseConfig.js │ │ │ │ │ ├── dev/ │ │ │ │ │ │ └── createConfigClient.js │ │ │ │ │ ├── getRouteBuildPath.js │ │ │ │ │ ├── getRouteBuildSubPath.js │ │ │ │ │ ├── isBuildRequired.ts │ │ │ │ │ ├── loaders/ │ │ │ │ │ │ ├── AreaLoader.js │ │ │ │ │ │ ├── GraphQLAPILoader.js │ │ │ │ │ │ ├── GraphqlLoader.js │ │ │ │ │ │ ├── LayoutLoader.js │ │ │ │ │ │ ├── StyleLoader.js │ │ │ │ │ │ ├── TailwindLoader.js │ │ │ │ │ │ ├── TranslationLoader.js │ │ │ │ │ │ └── loadTranslationFromCsv.ts │ │ │ │ │ ├── plugins/ │ │ │ │ │ │ ├── FileListPlugin.js │ │ │ │ │ │ ├── GraphqlPlugin.js │ │ │ │ │ │ ├── InjectTailwindSources.ts │ │ │ │ │ │ ├── Tailwindcss.ts │ │ │ │ │ │ └── ThemeWatcherPlugin.ts │ │ │ │ │ ├── prod/ │ │ │ │ │ │ ├── createConfigClient.js │ │ │ │ │ │ └── createConfigServer.js │ │ │ │ │ ├── resolveAlias.js │ │ │ │ │ ├── tests/ │ │ │ │ │ │ └── unit/ │ │ │ │ │ │ ├── resolveAlias.test.js │ │ │ │ │ │ └── theme/ │ │ │ │ │ │ └── components/ │ │ │ │ │ │ ├── a/ │ │ │ │ │ │ │ ├── A.jsx │ │ │ │ │ │ │ └── a.scss │ │ │ │ │ │ └── b/ │ │ │ │ │ │ ├── B.jsx │ │ │ │ │ │ ├── B.scss │ │ │ │ │ │ └── bb/ │ │ │ │ │ │ └── BB.jsx │ │ │ │ │ └── util/ │ │ │ │ │ ├── getTailwindConfig.js │ │ │ │ │ ├── getTailwindSources.ts │ │ │ │ │ ├── parseGraphql.js │ │ │ │ │ └── parseGraphqlByFile.js │ │ │ │ └── widget/ │ │ │ │ ├── tests/ │ │ │ │ │ └── unit/ │ │ │ │ │ └── widgetManager.test.js │ │ │ │ └── widgetManager.ts │ │ │ ├── modules/ │ │ │ │ ├── auth/ │ │ │ │ │ ├── api/ │ │ │ │ │ │ ├── getUserToken/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ │ ├── generateToken.ts │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── global/ │ │ │ │ │ │ │ ├── [context]getCurrentUser.ts │ │ │ │ │ │ │ ├── [context]jwtUserAuth[getCurrentUser].ts │ │ │ │ │ │ │ ├── [getCurrentUser]auth.ts │ │ │ │ │ │ │ └── [getCurrentUser]demoAccountBlocking[auth].ts │ │ │ │ │ │ └── refreshUserToken/ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ ├── refreshToken.ts │ │ │ │ │ │ └── route.json │ │ │ │ │ ├── bootstrap.js │ │ │ │ │ ├── graphql/ │ │ │ │ │ │ └── types/ │ │ │ │ │ │ └── AdminUser/ │ │ │ │ │ │ ├── AdminUser.admin.graphql │ │ │ │ │ │ └── AdminUser.admin.resolvers.js │ │ │ │ │ ├── migration/ │ │ │ │ │ │ ├── Version-1.0.0.js │ │ │ │ │ │ └── Version-1.0.1.js │ │ │ │ │ ├── pages/ │ │ │ │ │ │ └── admin/ │ │ │ │ │ │ ├── adminLogin/ │ │ │ │ │ │ │ ├── LoginForm.scss │ │ │ │ │ │ │ ├── LoginForm.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── adminLoginJson/ │ │ │ │ │ │ │ ├── [bodyParser]logIn.js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── adminLogoutJson/ │ │ │ │ │ │ │ ├── logout.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ └── all/ │ │ │ │ │ │ ├── AdminUser.jsx │ │ │ │ │ │ └── [context]auth.js │ │ │ │ │ └── services/ │ │ │ │ │ ├── getAdminSessionCookieName.ts │ │ │ │ │ ├── getCookieSecret.ts │ │ │ │ │ ├── getFrontStoreSessionCookieName.ts │ │ │ │ │ ├── getSessionConfig.ts │ │ │ │ │ ├── loginUserWithEmail.ts │ │ │ │ │ └── logoutUser.ts │ │ │ │ ├── base/ │ │ │ │ │ ├── api/ │ │ │ │ │ │ └── global/ │ │ │ │ │ │ ├── [apiResponse]apiErrorHandler.ts │ │ │ │ │ │ ├── [auth]apiResponse[apiErrorHandler].ts │ │ │ │ │ │ ├── [auth]payloadValidate.ts │ │ │ │ │ │ ├── [payloadValidate]escapeHtml.ts │ │ │ │ │ │ └── context.js │ │ │ │ │ ├── bootstrap.js │ │ │ │ │ ├── graphql/ │ │ │ │ │ │ └── types/ │ │ │ │ │ │ ├── Country/ │ │ │ │ │ │ │ ├── Country.graphql │ │ │ │ │ │ │ └── Country.resolvers.js │ │ │ │ │ │ ├── Currency/ │ │ │ │ │ │ │ ├── Currency.graphql │ │ │ │ │ │ │ └── Currency.resolvers.js │ │ │ │ │ │ ├── DateTime/ │ │ │ │ │ │ │ ├── DateTime.graphql │ │ │ │ │ │ │ └── DateTime.resolvers.js │ │ │ │ │ │ ├── Province/ │ │ │ │ │ │ │ ├── Province.graphql │ │ │ │ │ │ │ └── Province.resolvers.js │ │ │ │ │ │ ├── Route/ │ │ │ │ │ │ │ ├── Route.admin.graphql │ │ │ │ │ │ │ └── Route.admin.resolvers.js │ │ │ │ │ │ ├── Timezone/ │ │ │ │ │ │ │ ├── Timezone.graphql │ │ │ │ │ │ │ └── Timezone.resolvers.js │ │ │ │ │ │ ├── Url/ │ │ │ │ │ │ │ ├── Url.graphql │ │ │ │ │ │ │ └── Url.resolvers.js │ │ │ │ │ │ └── Version/ │ │ │ │ │ │ ├── Version.graphql │ │ │ │ │ │ └── Version.resolvers.js │ │ │ │ │ ├── migration/ │ │ │ │ │ │ └── Version-1.0.1.js │ │ │ │ │ ├── pages/ │ │ │ │ │ │ ├── admin/ │ │ │ │ │ │ │ └── all/ │ │ │ │ │ │ │ ├── FormCss.tsx │ │ │ │ │ │ │ ├── GlobalCss.tsx │ │ │ │ │ │ │ ├── Layout.tsx │ │ │ │ │ │ │ ├── Meta.tsx │ │ │ │ │ │ │ ├── TailwindCss.tsx │ │ │ │ │ │ │ ├── [context]isAdmin[auth].js │ │ │ │ │ │ │ ├── form.scss │ │ │ │ │ │ │ ├── global.scss │ │ │ │ │ │ │ ├── shadcn.css │ │ │ │ │ │ │ └── tailwind.css │ │ │ │ │ │ ├── frontStore/ │ │ │ │ │ │ │ └── all/ │ │ │ │ │ │ │ ├── Base.tsx │ │ │ │ │ │ │ ├── Breadcrumb.tsx │ │ │ │ │ │ │ ├── GlobalCss.tsx │ │ │ │ │ │ │ ├── HeadTags.tsx │ │ │ │ │ │ │ ├── Logo.tsx │ │ │ │ │ │ │ ├── Notification.scss │ │ │ │ │ │ │ ├── Notification.tsx │ │ │ │ │ │ │ ├── TailwindCss.tsx │ │ │ │ │ │ │ ├── global.scss │ │ │ │ │ │ │ ├── shadcn.css │ │ │ │ │ │ │ └── tailwind.css │ │ │ │ │ │ └── global/ │ │ │ │ │ │ ├── [auth]notFound[response].ts │ │ │ │ │ │ ├── [response]errorHandler.js │ │ │ │ │ │ ├── context.js │ │ │ │ │ │ └── response[errorHandler].ts │ │ │ │ │ └── services/ │ │ │ │ │ ├── escapePayload.ts │ │ │ │ │ ├── getAjv.js │ │ │ │ │ ├── markSkipEscape.ts │ │ │ │ │ ├── notifications.js │ │ │ │ │ └── secret.js │ │ │ │ ├── catalog/ │ │ │ │ │ ├── api/ │ │ │ │ │ │ ├── addProductToCategory/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── addProducts.js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── addProductToCollection/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── addProducts.js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── addVariantItem/ │ │ │ │ │ │ │ ├── [bodyParser]addItem.js │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── createAttribute/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── createAttribute[finish].js │ │ │ │ │ │ │ ├── finish[apiResponse].js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── createAttributeGroup/ │ │ │ │ │ │ │ ├── [bodyParser]saveGroup.js │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── createCategory/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ │ ├── createCategory[finish].ts │ │ │ │ │ │ │ ├── finish[apiResponse].ts │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── createCollection/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ │ ├── createCollection[finish].ts │ │ │ │ │ │ │ ├── finish[apiResponse].ts │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── createProduct/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ │ ├── createProduct[finish].ts │ │ │ │ │ │ │ ├── finish[apiResponse].ts │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── createVariantGroup/ │ │ │ │ │ │ │ ├── [bodyParser]saveGroup.js │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── deleteAttribute/ │ │ │ │ │ │ │ ├── deleteAttribute.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── deleteAttributeGroup/ │ │ │ │ │ │ │ ├── deleteAttributeGroup.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── deleteCategory/ │ │ │ │ │ │ │ ├── deleteCategory.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── deleteCollection/ │ │ │ │ │ │ │ ├── deleteCollection.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── deleteProduct/ │ │ │ │ │ │ │ ├── deleteProduct.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── removeProductFromCategory/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── removeProducts.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── removeProductFromCollection/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── removeProducts.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── unlinkVariant/ │ │ │ │ │ │ │ ├── [context]multerNone[auth].js │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── unlinkVariants.js │ │ │ │ │ │ ├── updateAttribute/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ │ ├── finish[apiResponse].ts │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── updateAttribute[finish].ts │ │ │ │ │ │ ├── updateAttributeGroup/ │ │ │ │ │ │ │ ├── [bodyParser]saveGroup.js │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── updateCategory/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ │ ├── finish[apiResponse].ts │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── updateCategory[finish].ts │ │ │ │ │ │ ├── updateCollection/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ │ ├── finish[apiResponse].ts │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── updateCollection[finish].ts │ │ │ │ │ │ ├── updateProduct/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ │ ├── finish[apiResponse].ts │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── updateProduct[finish].ts │ │ │ │ │ │ └── variantSearch/ │ │ │ │ │ │ ├── loadVariants.js │ │ │ │ │ │ └── route.json │ │ │ │ │ ├── bootstrap.js │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── CollectionProducts.tsx │ │ │ │ │ │ └── CollectionProductsSetting.tsx │ │ │ │ │ ├── graphql/ │ │ │ │ │ │ └── types/ │ │ │ │ │ │ ├── Attribute/ │ │ │ │ │ │ │ ├── Attribute.admin.graphql │ │ │ │ │ │ │ ├── Attribute.admin.resolvers.js │ │ │ │ │ │ │ ├── Attribute.graphql │ │ │ │ │ │ │ └── Attribute.resolvers.js │ │ │ │ │ │ ├── Category/ │ │ │ │ │ │ │ ├── Category.admin.graphql │ │ │ │ │ │ │ ├── Category.admin.resolvers.js │ │ │ │ │ │ │ ├── Category.graphql │ │ │ │ │ │ │ └── Category.resolvers.ts │ │ │ │ │ │ ├── Collection/ │ │ │ │ │ │ │ ├── Collection.admin.graphql │ │ │ │ │ │ │ ├── Collection.admin.resolvers.js │ │ │ │ │ │ │ ├── Collection.graphql │ │ │ │ │ │ │ └── Collection.resolvers.js │ │ │ │ │ │ ├── FeaturedProduct/ │ │ │ │ │ │ │ ├── FeaturedProduct.graphql │ │ │ │ │ │ │ └── FeaturedProduct.resolvers.js │ │ │ │ │ │ ├── Product/ │ │ │ │ │ │ │ ├── Attribute/ │ │ │ │ │ │ │ │ ├── ProductAttribute.graphql │ │ │ │ │ │ │ │ └── ProductAttribute.resolvers.js │ │ │ │ │ │ │ ├── CustomOption/ │ │ │ │ │ │ │ │ ├── CustomOption.graphql │ │ │ │ │ │ │ │ └── CustomOption.resolvers.js │ │ │ │ │ │ │ ├── Image/ │ │ │ │ │ │ │ │ ├── ProductImage.graphql │ │ │ │ │ │ │ │ └── ProductImage.resolvers.ts │ │ │ │ │ │ │ ├── Inventory/ │ │ │ │ │ │ │ │ ├── Inventory.admin.graphql │ │ │ │ │ │ │ │ ├── Inventory.admin.resolvers.js │ │ │ │ │ │ │ │ ├── Inventory.graphql │ │ │ │ │ │ │ │ └── Inventory.resolvers.js │ │ │ │ │ │ │ ├── Price/ │ │ │ │ │ │ │ │ ├── ProductPrice.graphql │ │ │ │ │ │ │ │ └── ProductPrice.resolvers.js │ │ │ │ │ │ │ ├── Product.admin.graphql │ │ │ │ │ │ │ ├── Product.admin.resolvers.ts │ │ │ │ │ │ │ ├── Product.graphql │ │ │ │ │ │ │ ├── Product.resolvers.ts │ │ │ │ │ │ │ └── Variant/ │ │ │ │ │ │ │ ├── Variant.graphql │ │ │ │ │ │ │ └── Variant.resolvers.js │ │ │ │ │ │ └── Widget/ │ │ │ │ │ │ └── CollectionProductsWidget/ │ │ │ │ │ │ ├── CollectionProductsWidget.graphql │ │ │ │ │ │ └── CollectionProductsWidget.resolvers.js │ │ │ │ │ ├── migration/ │ │ │ │ │ │ ├── Version-1.0.0.js │ │ │ │ │ │ ├── Version-1.0.1.js │ │ │ │ │ │ ├── Version-1.0.2.js │ │ │ │ │ │ ├── Version-1.0.3.js │ │ │ │ │ │ ├── Version-1.0.4.js │ │ │ │ │ │ ├── Version-1.0.5.js │ │ │ │ │ │ ├── Version-1.0.6.js │ │ │ │ │ │ ├── Version-1.0.7.js │ │ │ │ │ │ └── Version-1.0.8.ts │ │ │ │ │ ├── pages/ │ │ │ │ │ │ ├── admin/ │ │ │ │ │ │ │ ├── all/ │ │ │ │ │ │ │ │ ├── CatalogMenuGroup.jsx │ │ │ │ │ │ │ │ └── NewProductQuickLink.jsx │ │ │ │ │ │ │ ├── attributeEdit/ │ │ │ │ │ │ │ │ ├── AttributeEditForm.tsx │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ │ ├── attributeEdit+attributeNew/ │ │ │ │ │ │ │ │ ├── Avaibility.tsx │ │ │ │ │ │ │ │ ├── General.scss │ │ │ │ │ │ │ │ ├── General.tsx │ │ │ │ │ │ │ │ └── PageHeading.tsx │ │ │ │ │ │ │ ├── attributeGrid/ │ │ │ │ │ │ │ │ ├── Grid.jsx │ │ │ │ │ │ │ │ ├── NewAttributeButton.tsx │ │ │ │ │ │ │ │ ├── PageHeading.tsx │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ │ └── rows/ │ │ │ │ │ │ │ │ ├── AttributeName.tsx │ │ │ │ │ │ │ │ └── GroupRow.tsx │ │ │ │ │ │ │ ├── attributeNew/ │ │ │ │ │ │ │ │ ├── AttributeNewForm.tsx │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ │ ├── categoryEdit/ │ │ │ │ │ │ │ │ ├── CategoryEditForm.tsx │ │ │ │ │ │ │ │ ├── Products.tsx │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ │ ├── categoryEdit+categoryNew/ │ │ │ │ │ │ │ │ ├── General.scss │ │ │ │ │ │ │ │ ├── General.tsx │ │ │ │ │ │ │ │ ├── Image.scss │ │ │ │ │ │ │ │ ├── Image.tsx │ │ │ │ │ │ │ │ ├── PageHeading.tsx │ │ │ │ │ │ │ │ ├── Seo.tsx │ │ │ │ │ │ │ │ └── Status.tsx │ │ │ │ │ │ │ ├── categoryGrid/ │ │ │ │ │ │ │ │ ├── Grid.jsx │ │ │ │ │ │ │ │ ├── NewCategoryButton.tsx │ │ │ │ │ │ │ │ ├── PageHeading.tsx │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ │ └── rows/ │ │ │ │ │ │ │ │ └── CategoryName.tsx │ │ │ │ │ │ │ ├── categoryNew/ │ │ │ │ │ │ │ │ ├── CategoryNewForm.tsx │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ │ ├── collectionEdit/ │ │ │ │ │ │ │ │ ├── CollectionEditForm.tsx │ │ │ │ │ │ │ │ ├── Products.tsx │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ │ ├── collectionEdit+collectionNew/ │ │ │ │ │ │ │ │ ├── General.scss │ │ │ │ │ │ │ │ ├── General.tsx │ │ │ │ │ │ │ │ └── PageHeading.tsx │ │ │ │ │ │ │ ├── collectionGrid/ │ │ │ │ │ │ │ │ ├── Grid.jsx │ │ │ │ │ │ │ │ ├── NewCollectionButton.tsx │ │ │ │ │ │ │ │ ├── PageHeading.tsx │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ │ └── rows/ │ │ │ │ │ │ │ │ └── CollectionNameRow.tsx │ │ │ │ │ │ │ ├── collectionNew/ │ │ │ │ │ │ │ │ ├── CollectionNewForm.tsx │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ │ ├── productEdit/ │ │ │ │ │ │ │ │ ├── Collection.tsx │ │ │ │ │ │ │ │ ├── ProductEditForm.tsx │ │ │ │ │ │ │ │ ├── VariantGroup.tsx │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ │ └── variants/ │ │ │ │ │ │ │ │ ├── CreateVariant.tsx │ │ │ │ │ │ │ │ ├── CreateVariantGroup.tsx │ │ │ │ │ │ │ │ ├── EditVariant.tsx │ │ │ │ │ │ │ │ ├── New.tsx │ │ │ │ │ │ │ │ ├── Skeleton.tsx │ │ │ │ │ │ │ │ ├── Variant.tsx │ │ │ │ │ │ │ │ ├── VariantModal.tsx │ │ │ │ │ │ │ │ └── Variants.tsx │ │ │ │ │ │ │ ├── productEdit+productNew/ │ │ │ │ │ │ │ │ ├── Attributes.tsx │ │ │ │ │ │ │ │ ├── General.scss │ │ │ │ │ │ │ │ ├── General.tsx │ │ │ │ │ │ │ │ ├── Inventory.tsx │ │ │ │ │ │ │ │ ├── Media.tsx │ │ │ │ │ │ │ │ ├── PageHeading.tsx │ │ │ │ │ │ │ │ ├── Seo.tsx │ │ │ │ │ │ │ │ ├── Shipping.tsx │ │ │ │ │ │ │ │ └── Status.tsx │ │ │ │ │ │ │ ├── productGrid/ │ │ │ │ │ │ │ │ ├── Grid.jsx │ │ │ │ │ │ │ │ ├── NewProductButton.tsx │ │ │ │ │ │ │ │ ├── PageHeading.tsx │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ │ └── rows/ │ │ │ │ │ │ │ │ └── ProductName.tsx │ │ │ │ │ │ │ └── productNew/ │ │ │ │ │ │ │ ├── ProductNewForm.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ └── frontStore/ │ │ │ │ │ │ ├── all/ │ │ │ │ │ │ │ └── SearchBox.tsx │ │ │ │ │ │ ├── catalogSearch/ │ │ │ │ │ │ │ ├── SearchPage.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── categoryView/ │ │ │ │ │ │ │ ├── CategoryView.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ └── productView/ │ │ │ │ │ │ ├── ProductView.tsx │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── route.json │ │ │ │ │ ├── services/ │ │ │ │ │ │ ├── AttributeCollection.js │ │ │ │ │ │ ├── AttributeGroupCollection.js │ │ │ │ │ │ ├── CategoryCollection.js │ │ │ │ │ │ ├── CollectionCollection.js │ │ │ │ │ │ ├── ProductCollection.js │ │ │ │ │ │ ├── attribute/ │ │ │ │ │ │ │ ├── attributeDataSchema.json │ │ │ │ │ │ │ ├── createProductAttribute.ts │ │ │ │ │ │ │ ├── deleteProductAttribute.ts │ │ │ │ │ │ │ └── updateProductAttribute.ts │ │ │ │ │ │ ├── category/ │ │ │ │ │ │ │ ├── categoryDataSchema.json │ │ │ │ │ │ │ ├── createCategory.ts │ │ │ │ │ │ │ ├── deleteCategory.ts │ │ │ │ │ │ │ └── updateCategory.ts │ │ │ │ │ │ ├── collection/ │ │ │ │ │ │ │ ├── createCollection.ts │ │ │ │ │ │ │ ├── deleteCollection.ts │ │ │ │ │ │ │ └── updateCollection.ts │ │ │ │ │ │ ├── getAttributeGroupsBaseQuery.js │ │ │ │ │ │ ├── getAttributesBaseQuery.js │ │ │ │ │ │ ├── getCategoriesBaseQuery.ts │ │ │ │ │ │ ├── getCollectionsBaseQuery.ts │ │ │ │ │ │ ├── getFilterableAttributes.js │ │ │ │ │ │ ├── getProductsBaseQuery.ts │ │ │ │ │ │ ├── getProductsByCategoryBaseQuery.ts │ │ │ │ │ │ ├── getProductsByCollectionBaseQuery.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── product/ │ │ │ │ │ │ │ ├── createProduct.ts │ │ │ │ │ │ │ ├── deleteProduct.ts │ │ │ │ │ │ │ ├── productDataSchema.json │ │ │ │ │ │ │ └── updateProduct.ts │ │ │ │ │ │ ├── registerCartItemProductUrlField.js │ │ │ │ │ │ ├── registerCartItemVariantOptionsField.js │ │ │ │ │ │ ├── registerDefaultAttributeCollectionFilters.js │ │ │ │ │ │ ├── registerDefaultCategoryCollectionFilters.js │ │ │ │ │ │ ├── registerDefaultCollectionCollectionFilters.js │ │ │ │ │ │ └── registerDefaultProductCollectionFilters.js │ │ │ │ │ ├── subscribers/ │ │ │ │ │ │ ├── category_created/ │ │ │ │ │ │ │ └── buildUrlRewrite.ts │ │ │ │ │ │ ├── category_deleted/ │ │ │ │ │ │ │ └── deleteUrlRewrite.ts │ │ │ │ │ │ ├── category_updated/ │ │ │ │ │ │ │ └── builUrlRewrite.ts │ │ │ │ │ │ ├── product_created/ │ │ │ │ │ │ │ └── buildUrlRewrite.ts │ │ │ │ │ │ ├── product_deleted/ │ │ │ │ │ │ │ └── deleteUrlRewrite.ts │ │ │ │ │ │ └── product_updated/ │ │ │ │ │ │ └── buildUrlRewrite.ts │ │ │ │ │ └── tests/ │ │ │ │ │ └── intergration/ │ │ │ │ │ └── productView.test.js │ │ │ │ ├── checkout/ │ │ │ │ │ ├── api/ │ │ │ │ │ │ ├── addCartAddress/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── saveAddress.ts │ │ │ │ │ │ ├── addCartContactInfo/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── saveContactInfo.js │ │ │ │ │ │ ├── addCartItem/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── addItemToCart.js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── addCartPaymentMethod/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── savePaymentMethod.js │ │ │ │ │ │ ├── addCartShippingMethod/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── saveShippingMethod.js │ │ │ │ │ │ ├── addMineCartItem/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── addItemToCart.ts │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── addShippingNote/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── saveShippingNote.js │ │ │ │ │ │ ├── addShippingZoneMethod/ │ │ │ │ │ │ │ ├── [context]borderParser[auth].js │ │ │ │ │ │ │ ├── [validateMethod]addShippingZoneMethod.js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── validateMethod.js │ │ │ │ │ │ ├── cartCheckout/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ │ ├── checkout.ts │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── createCart/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── createNewCart.js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── createOrder/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ ├── placeOrder.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── createShippingMethod/ │ │ │ │ │ │ │ ├── [context]borderParser[auth].js │ │ │ │ │ │ │ ├── createShippingMethod.js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── createShippingZone/ │ │ │ │ │ │ │ ├── [context]borderParser[auth].js │ │ │ │ │ │ │ ├── createShippingZone.js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── deleteShippingZone/ │ │ │ │ │ │ │ ├── deleteShippingZone.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── deleteShippingZoneMethod/ │ │ │ │ │ │ │ ├── deleteShippingZoneMethod.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── removeCartItem/ │ │ │ │ │ │ │ ├── removeItem.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── removeMineCartItem/ │ │ │ │ │ │ │ ├── removeItem.ts │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── updateCartItemQty/ │ │ │ │ │ │ │ ├── [bodyParser]updateQty.js │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── updateMineCartItemQty/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── updateQty.ts │ │ │ │ │ │ ├── updateShippingMethod/ │ │ │ │ │ │ │ ├── [context]borderParser[auth].js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── updateShippingMethod.js │ │ │ │ │ │ ├── updateShippingZone/ │ │ │ │ │ │ │ ├── [context]borderParser[auth].js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── updateShippingZone.js │ │ │ │ │ │ └── updateShippingZoneMethod/ │ │ │ │ │ │ ├── [context]borderParser[auth].js │ │ │ │ │ │ ├── [validateMethod]updateShippingZoneMethod.js │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ └── validateMethod.js │ │ │ │ │ ├── bootstrap.ts │ │ │ │ │ ├── graphql/ │ │ │ │ │ │ └── types/ │ │ │ │ │ │ ├── Cart/ │ │ │ │ │ │ │ ├── Cart.graphql │ │ │ │ │ │ │ └── Cart.resolvers.ts │ │ │ │ │ │ ├── CheckoutSetting/ │ │ │ │ │ │ │ ├── CheckoutSetting.graphql │ │ │ │ │ │ │ └── CheckoutSetting.resolvers.js │ │ │ │ │ │ ├── Date/ │ │ │ │ │ │ │ ├── Date.graphql │ │ │ │ │ │ │ └── Date.resolvers.js │ │ │ │ │ │ ├── PaymentMethod/ │ │ │ │ │ │ │ ├── AvailablePaymentMethod.graphql │ │ │ │ │ │ │ └── AvailablePaymentMethod.resolvers.ts │ │ │ │ │ │ ├── Price/ │ │ │ │ │ │ │ ├── Price.graphql │ │ │ │ │ │ │ └── Price.resolvers.js │ │ │ │ │ │ ├── ShippingMethod/ │ │ │ │ │ │ │ ├── AvailableShippingMethod.graphql │ │ │ │ │ │ │ ├── AvailableShippingMethod.resolvers.ts │ │ │ │ │ │ │ ├── ShippingMethod.admin.graphql │ │ │ │ │ │ │ └── ShippingMethod.admin.resolvers.js │ │ │ │ │ │ ├── ShippingZone/ │ │ │ │ │ │ │ ├── ShippingZone.graphql │ │ │ │ │ │ │ └── ShippingZone.resolvers.js │ │ │ │ │ │ └── Weight/ │ │ │ │ │ │ ├── Weight.graphql │ │ │ │ │ │ └── Weight.resolvers.js │ │ │ │ │ ├── migration/ │ │ │ │ │ │ ├── Version-1.0.0.js │ │ │ │ │ │ ├── Version-1.0.1.js │ │ │ │ │ │ ├── Version-1.0.2.js │ │ │ │ │ │ ├── Version-1.0.3.js │ │ │ │ │ │ ├── Version-1.0.4.js │ │ │ │ │ │ ├── Version-1.0.5.js │ │ │ │ │ │ ├── Version-1.0.6.js │ │ │ │ │ │ └── Version-1.0.7.ts │ │ │ │ │ ├── pages/ │ │ │ │ │ │ ├── admin/ │ │ │ │ │ │ │ ├── all/ │ │ │ │ │ │ │ │ └── ShippingSettingMenu.tsx │ │ │ │ │ │ │ └── shippingSetting/ │ │ │ │ │ │ │ ├── ShippingSetting.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── shippingSetting/ │ │ │ │ │ │ │ ├── Method.tsx │ │ │ │ │ │ │ ├── MethodForm.tsx │ │ │ │ │ │ │ ├── Methods.tsx │ │ │ │ │ │ │ ├── PriceBasedPrice.tsx │ │ │ │ │ │ │ ├── WeightBasedPrice.tsx │ │ │ │ │ │ │ ├── Zone.tsx │ │ │ │ │ │ │ ├── ZoneForm.tsx │ │ │ │ │ │ │ └── Zones.tsx │ │ │ │ │ │ └── frontStore/ │ │ │ │ │ │ ├── all/ │ │ │ │ │ │ │ ├── MiniCartIcon.tsx │ │ │ │ │ │ │ └── [auth]addCustomerToCart.ts │ │ │ │ │ │ ├── cart/ │ │ │ │ │ │ │ ├── ShoppingCart.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── checkout/ │ │ │ │ │ │ │ ├── Checkout.scss │ │ │ │ │ │ │ ├── Checkout.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ └── checkoutSuccess/ │ │ │ │ │ │ ├── CheckoutSuccess.jsx │ │ │ │ │ │ ├── CheckoutSuccess.scss │ │ │ │ │ │ ├── CustomerInfo.tsx │ │ │ │ │ │ ├── ShippingNote.tsx │ │ │ │ │ │ ├── Summary.scss │ │ │ │ │ │ ├── Summary.tsx │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── route.json │ │ │ │ │ ├── services/ │ │ │ │ │ │ ├── addBillingAddress.ts │ │ │ │ │ │ ├── addCartItem.ts │ │ │ │ │ │ ├── addShippingAddress.ts │ │ │ │ │ │ ├── cart/ │ │ │ │ │ │ │ ├── Cart.js │ │ │ │ │ │ │ ├── DataObject.js │ │ │ │ │ │ │ ├── registerCartBaseFields.js │ │ │ │ │ │ │ ├── registerCartItemBaseFields.js │ │ │ │ │ │ │ └── sortFields.js │ │ │ │ │ │ ├── checkout.ts │ │ │ │ │ │ ├── createNewCart.ts │ │ │ │ │ │ ├── getAvailablePaymentMethods.ts │ │ │ │ │ │ ├── getAvailableShippingMethods.ts │ │ │ │ │ │ ├── getCartByUUID.ts │ │ │ │ │ │ ├── getMyCart.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── orderCreator.ts │ │ │ │ │ │ ├── orderValidator.ts │ │ │ │ │ │ ├── removeCartItem.ts │ │ │ │ │ │ ├── saveCart.ts │ │ │ │ │ │ ├── toPrice.ts │ │ │ │ │ │ └── updateCartItemQty.ts │ │ │ │ │ └── tests/ │ │ │ │ │ ├── basicSetup.js │ │ │ │ │ ├── coupons.js │ │ │ │ │ ├── products.js │ │ │ │ │ ├── taxRates.js │ │ │ │ │ └── unit/ │ │ │ │ │ ├── addItemSideEffect.test.js │ │ │ │ │ ├── discountAmount.test.js │ │ │ │ │ ├── grandTotal.test.js │ │ │ │ │ ├── lineTotal.test.js │ │ │ │ │ ├── lineTotalWithDiscount.test.js │ │ │ │ │ ├── productPrice.test.js │ │ │ │ │ ├── removeItemSideEffect.test.js │ │ │ │ │ ├── subTotal.test.js │ │ │ │ │ ├── subTotalWithDiscount.test.js │ │ │ │ │ ├── taxAmount.test.js │ │ │ │ │ ├── taxAmountRounding.test.js │ │ │ │ │ └── updateCartItemQtySideEffect.test.js │ │ │ │ ├── cms/ │ │ │ │ │ ├── api/ │ │ │ │ │ │ ├── createCmsPage/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ │ ├── createPage[finish].ts │ │ │ │ │ │ │ ├── finish[apiResponse].ts │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── createWidget/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── createWidget[finish].js │ │ │ │ │ │ │ ├── finish[apiResponse].js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── deleteCmsPage/ │ │ │ │ │ │ │ ├── deleteCmsPage.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── deleteWidget/ │ │ │ │ │ │ │ ├── deleteWidget.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── fileBrowser/ │ │ │ │ │ │ │ ├── [context]validatePath[auth].js │ │ │ │ │ │ │ ├── browFiles.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── fileDelete/ │ │ │ │ │ │ │ ├── [context]validatePath[auth].js │ │ │ │ │ │ │ ├── deleteFile.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── fileUpload/ │ │ │ │ │ │ │ ├── [auth,validatePath]multerFile.js │ │ │ │ │ │ │ ├── [multerFile]upload.js │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── validatePath.js │ │ │ │ │ │ ├── folderCreate/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── [context]validatePath[auth].js │ │ │ │ │ │ │ ├── createFolder.js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── imageUpload/ │ │ │ │ │ │ │ ├── [auth,validatePath]multerFile.js │ │ │ │ │ │ │ ├── [multerFile]upload.js │ │ │ │ │ │ │ ├── [multerFile]verifyImages[upload].js │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── validatePath.js │ │ │ │ │ │ ├── updateCmsPage/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ │ ├── finish[apiResponse].ts │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── updatePage[finish].ts │ │ │ │ │ │ └── updateWidget/ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ ├── finish[apiResponse].ts │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ └── updateWidget[finish].ts │ │ │ │ │ ├── bootstrap.ts │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── Banner.tsx │ │ │ │ │ │ ├── BannerSetting.tsx │ │ │ │ │ │ ├── BasicMenu.tsx │ │ │ │ │ │ ├── BasicMenuSetting.scss │ │ │ │ │ │ ├── BasicMenuSetting.tsx │ │ │ │ │ │ ├── Slideshow.tsx │ │ │ │ │ │ ├── SlideshowSetting.tsx │ │ │ │ │ │ ├── TextBlock.tsx │ │ │ │ │ │ └── TextBlockSetting.tsx │ │ │ │ │ ├── graphql/ │ │ │ │ │ │ └── types/ │ │ │ │ │ │ ├── CmsPage/ │ │ │ │ │ │ │ ├── CmsPage.graphql │ │ │ │ │ │ │ └── CmsPage.resolvers.ts │ │ │ │ │ │ ├── Menu/ │ │ │ │ │ │ │ ├── Menu.graphql │ │ │ │ │ │ │ └── Menu.resolvers.js │ │ │ │ │ │ ├── PageInfo/ │ │ │ │ │ │ │ ├── PageInfo.graphql │ │ │ │ │ │ │ └── PageInfo.resolvers.ts │ │ │ │ │ │ ├── ThemeConfig/ │ │ │ │ │ │ │ ├── ThemeConfig.graphql │ │ │ │ │ │ │ └── ThemeConfig.resolvers.js │ │ │ │ │ │ └── Widget/ │ │ │ │ │ │ ├── Widget.graphql │ │ │ │ │ │ └── Widget.resolvers.js │ │ │ │ │ ├── migration/ │ │ │ │ │ │ ├── Version-1.0.0.js │ │ │ │ │ │ ├── Version-1.1.0.js │ │ │ │ │ │ └── Version-1.1.1.js │ │ │ │ │ ├── pages/ │ │ │ │ │ │ ├── admin/ │ │ │ │ │ │ │ ├── adminNotFound/ │ │ │ │ │ │ │ │ ├── Meta.tsx │ │ │ │ │ │ │ │ ├── NotFound.tsx │ │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ │ ├── adminStaticAsset/ │ │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ │ └── staticAssets.ts │ │ │ │ │ │ │ ├── all/ │ │ │ │ │ │ │ │ ├── CmsMenuGroup.jsx │ │ │ │ │ │ │ │ ├── CopyRight.tsx │ │ │ │ │ │ │ │ ├── Logo.tsx │ │ │ │ │ │ │ │ ├── Navigation.jsx │ │ │ │ │ │ │ │ ├── Navigation.scss │ │ │ │ │ │ │ │ ├── Notification.jsx │ │ │ │ │ │ │ │ ├── Notification.scss │ │ │ │ │ │ │ │ ├── QuickLinks.jsx │ │ │ │ │ │ │ │ ├── SearchBox.tsx │ │ │ │ │ │ │ │ ├── Survey.tsx │ │ │ │ │ │ │ │ ├── Version.jsx │ │ │ │ │ │ │ │ └── search/ │ │ │ │ │ │ │ │ ├── NoResult.tsx │ │ │ │ │ │ │ │ └── Results.jsx │ │ │ │ │ │ │ ├── cmsPageEdit/ │ │ │ │ │ │ │ │ ├── PageEditForm.tsx │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ │ ├── cmsPageEdit+cmsPageNew/ │ │ │ │ │ │ │ │ ├── General.tsx │ │ │ │ │ │ │ │ ├── PageHeading.tsx │ │ │ │ │ │ │ │ └── Seo.tsx │ │ │ │ │ │ │ ├── cmsPageGrid/ │ │ │ │ │ │ │ │ ├── Grid.jsx │ │ │ │ │ │ │ │ ├── NewPageButton.tsx │ │ │ │ │ │ │ │ ├── PageHeading.tsx │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ │ └── rows/ │ │ │ │ │ │ │ │ └── PageName.tsx │ │ │ │ │ │ │ ├── cmsPageNew/ │ │ │ │ │ │ │ │ ├── PageNewForm.tsx │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ │ │ ├── Layout.jsx │ │ │ │ │ │ │ │ ├── Layout.scss │ │ │ │ │ │ │ │ ├── PageHeading.tsx │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ │ ├── widgetEdit/ │ │ │ │ │ │ │ │ ├── WidgetEditForm.tsx │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ │ ├── widgetEdit+widgetNew/ │ │ │ │ │ │ │ │ ├── General.tsx │ │ │ │ │ │ │ │ ├── PageHeading.tsx │ │ │ │ │ │ │ │ └── Setting.tsx │ │ │ │ │ │ │ ├── widgetGrid/ │ │ │ │ │ │ │ │ ├── Grid.jsx │ │ │ │ │ │ │ │ ├── Heading.tsx │ │ │ │ │ │ │ │ ├── NewWidgetButton.tsx │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ │ └── rows/ │ │ │ │ │ │ │ │ ├── Name.tsx │ │ │ │ │ │ │ │ └── WidgetTypeRow.tsx │ │ │ │ │ │ │ └── widgetNew/ │ │ │ │ │ │ │ ├── WidgetNewForm.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── typeValidate.js │ │ │ │ │ │ └── frontStore/ │ │ │ │ │ │ ├── cmsPageView/ │ │ │ │ │ │ │ ├── CmsPageView.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── homepage/ │ │ │ │ │ │ │ ├── meta.ts │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── images/ │ │ │ │ │ │ │ ├── images.ts │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── notFound/ │ │ │ │ │ │ │ ├── Meta.tsx │ │ │ │ │ │ │ ├── NotFound.tsx │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ └── staticAsset/ │ │ │ │ │ │ ├── [context]staticAssets[auth].ts │ │ │ │ │ │ └── route.json │ │ │ │ │ └── services/ │ │ │ │ │ ├── CMSPageCollection.js │ │ │ │ │ ├── CustomMemoryStorage.js │ │ │ │ │ ├── WidgetCollection.js │ │ │ │ │ ├── browFiles.ts │ │ │ │ │ ├── createFolder.ts │ │ │ │ │ ├── deleteFile.ts │ │ │ │ │ ├── generateFileName.js │ │ │ │ │ ├── getCmsPagesBaseQuery.js │ │ │ │ │ ├── getMulter.js │ │ │ │ │ ├── getWidgetsBaseQuery.js │ │ │ │ │ ├── imageProcessor.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── page/ │ │ │ │ │ │ ├── createPage.ts │ │ │ │ │ │ ├── deletePage.ts │ │ │ │ │ │ ├── pageDataSchema.json │ │ │ │ │ │ └── updatePage.ts │ │ │ │ │ ├── pageMetaInfo.ts │ │ │ │ │ ├── registerDefaultPageCollectionFilters.js │ │ │ │ │ ├── registerDefaultWidgetCollectionFilters.js │ │ │ │ │ ├── tailwind.admin.config.ts │ │ │ │ │ ├── tailwind.frontStore.config.ts │ │ │ │ │ ├── tests/ │ │ │ │ │ │ └── unit/ │ │ │ │ │ │ ├── imageProcessor.test.js │ │ │ │ │ │ ├── root/ │ │ │ │ │ │ │ ├── images/ │ │ │ │ │ │ │ │ ├── .gitkeep │ │ │ │ │ │ │ │ └── junk.txt │ │ │ │ │ │ │ └── media/ │ │ │ │ │ │ │ ├── cache/ │ │ │ │ │ │ │ │ ├── .gitkeep │ │ │ │ │ │ │ │ └── junk.txt │ │ │ │ │ │ │ └── images/ │ │ │ │ │ │ │ ├── .gitkeep │ │ │ │ │ │ │ └── junk.txt │ │ │ │ │ │ └── validatePath.test.js │ │ │ │ │ ├── uploadFile.ts │ │ │ │ │ ├── validatePath.js │ │ │ │ │ └── widget/ │ │ │ │ │ ├── createWidget.js │ │ │ │ │ ├── deleteWidget.js │ │ │ │ │ ├── loadWidgetInstances.js │ │ │ │ │ ├── updateWidget.js │ │ │ │ │ └── widgetDataSchema.json │ │ │ │ ├── cod/ │ │ │ │ │ ├── api/ │ │ │ │ │ │ └── codCapturePayment/ │ │ │ │ │ │ ├── [bodyParser]capture.js │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ └── route.json │ │ │ │ │ ├── bootstrap.ts │ │ │ │ │ ├── graphql/ │ │ │ │ │ │ └── types/ │ │ │ │ │ │ └── CODSetting/ │ │ │ │ │ │ ├── CODSetting.graphql │ │ │ │ │ │ └── CODSetting.resolvers.js │ │ │ │ │ └── pages/ │ │ │ │ │ ├── admin/ │ │ │ │ │ │ ├── orderEdit/ │ │ │ │ │ │ │ └── CaptureButton.jsx │ │ │ │ │ │ └── paymentSetting/ │ │ │ │ │ │ └── CODSetting.tsx │ │ │ │ │ └── frontStore/ │ │ │ │ │ └── checkout/ │ │ │ │ │ └── CashOnDelivery.tsx │ │ │ │ ├── customer/ │ │ │ │ │ ├── api/ │ │ │ │ │ │ ├── createCustomer/ │ │ │ │ │ │ │ ├── [bodyParser]createCustomer.ts │ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── createCustomerAddress/ │ │ │ │ │ │ │ ├── [bodyParser]createCustomerAddress.js │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── deleteCustomer/ │ │ │ │ │ │ │ ├── deleteCustomer.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── deleteCustomerAddress/ │ │ │ │ │ │ │ ├── deleteCustomerAddress.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── getCustomerToken/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ │ ├── generateToken.ts │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── global/ │ │ │ │ │ │ │ ├── [context]getCurrentCustomer[auth].js │ │ │ │ │ │ │ └── [context]jwtCustomerAuth[getCurrentCustomer].ts │ │ │ │ │ │ ├── refreshCustomerToken/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ ├── refreshToken.ts │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── resetPassword/ │ │ │ │ │ │ │ ├── [bodyParser]resetPassword.ts │ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── updateCustomer/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── updateCustomer.js │ │ │ │ │ │ ├── updateCustomerAddress/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── updateCustomerAddress.js │ │ │ │ │ │ └── updatePassword/ │ │ │ │ │ │ ├── [bodyParser]updatePassword.js │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ └── route.json │ │ │ │ │ ├── bootstrap.ts │ │ │ │ │ ├── graphql/ │ │ │ │ │ │ └── types/ │ │ │ │ │ │ ├── Customer/ │ │ │ │ │ │ │ ├── Customer.admin.graphql │ │ │ │ │ │ │ ├── Customer.admin.resolvers.js │ │ │ │ │ │ │ ├── Customer.graphql │ │ │ │ │ │ │ └── Customer.resolvers.ts │ │ │ │ │ │ └── CustomerGroup/ │ │ │ │ │ │ ├── CustomerGroup.admin.graphql │ │ │ │ │ │ ├── CustomerGroup.admin.resolvers.js │ │ │ │ │ │ ├── CustomerGroup.graphql │ │ │ │ │ │ └── CustomerGroup.resolvers.js │ │ │ │ │ ├── migration/ │ │ │ │ │ │ ├── Version-1.0.0.js │ │ │ │ │ │ ├── Version-1.0.1.js │ │ │ │ │ │ ├── Version-1.0.2.js │ │ │ │ │ │ └── Version-1.0.3.js │ │ │ │ │ ├── pages/ │ │ │ │ │ │ ├── admin/ │ │ │ │ │ │ │ ├── all/ │ │ │ │ │ │ │ │ └── CustomerMenuGroup.jsx │ │ │ │ │ │ │ ├── customerEdit/ │ │ │ │ │ │ │ │ ├── CustomerEditForm.jsx │ │ │ │ │ │ │ │ ├── CustomerEditForm.scss │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ │ ├── customerEdit+customerNew/ │ │ │ │ │ │ │ │ ├── General.jsx │ │ │ │ │ │ │ │ ├── OrderHistory.jsx │ │ │ │ │ │ │ │ └── PageHeading.tsx │ │ │ │ │ │ │ └── customerGrid/ │ │ │ │ │ │ │ ├── Grid.jsx │ │ │ │ │ │ │ ├── Heading.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── rows/ │ │ │ │ │ │ │ └── CustomerName.tsx │ │ │ │ │ │ └── frontStore/ │ │ │ │ │ │ ├── account/ │ │ │ │ │ │ │ ├── MyAccount.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── all/ │ │ │ │ │ │ │ ├── CustomerIcon.tsx │ │ │ │ │ │ │ └── [context]auth.js │ │ │ │ │ │ ├── customerLoginJson/ │ │ │ │ │ │ │ ├── [bodyParser]login.ts │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── customerLogoutJson/ │ │ │ │ │ │ │ ├── logout.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── login/ │ │ │ │ │ │ │ ├── LoginPage.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── register/ │ │ │ │ │ │ │ ├── RegisterPage.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ └── resetPasswordPage/ │ │ │ │ │ │ ├── ResetPasswordPage.tsx │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── route.json │ │ │ │ │ ├── services/ │ │ │ │ │ │ ├── CustomerCollection.js │ │ │ │ │ │ ├── CustomerGroupCollection.js │ │ │ │ │ │ ├── customer/ │ │ │ │ │ │ │ ├── address/ │ │ │ │ │ │ │ │ ├── addressValidators.ts │ │ │ │ │ │ │ │ ├── createCustomerAddress.ts │ │ │ │ │ │ │ │ ├── deleteCustomerAddress.ts │ │ │ │ │ │ │ │ └── updateCustomerAddress.ts │ │ │ │ │ │ │ ├── createCustomer.ts │ │ │ │ │ │ │ ├── customerDataSchema.json │ │ │ │ │ │ │ ├── deleteCustomer.ts │ │ │ │ │ │ │ ├── loginCustomerWithEmail.ts │ │ │ │ │ │ │ ├── logoutCustomer.js │ │ │ │ │ │ │ ├── updateCustomer.ts │ │ │ │ │ │ │ └── updatePassword.ts │ │ │ │ │ │ ├── getCustomerGroupsBaseQuery.js │ │ │ │ │ │ ├── getCustomersBaseQuery.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── registerDefaultCustomerCollectionFilters.js │ │ │ │ │ │ ├── registerDefaultCustomerGroupCollectionFilters.js │ │ │ │ │ │ └── sendResetPasswordEmail.ts │ │ │ │ │ └── subscribers/ │ │ │ │ │ └── customer_registered/ │ │ │ │ │ └── sendWelcomeEmail.ts │ │ │ │ ├── graphql/ │ │ │ │ │ ├── api/ │ │ │ │ │ │ ├── adminGraphql/ │ │ │ │ │ │ │ ├── [bodyParser]graphql.js │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ └── graphql/ │ │ │ │ │ │ ├── [auth]removeUser[graphql].ts │ │ │ │ │ │ ├── [bodyParser]graphql.js │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ └── route.json │ │ │ │ │ ├── bootstrap.js │ │ │ │ │ ├── graphql/ │ │ │ │ │ │ └── types/ │ │ │ │ │ │ └── Query/ │ │ │ │ │ │ ├── Query.graphql │ │ │ │ │ │ └── Query.resolvers.js │ │ │ │ │ ├── pages/ │ │ │ │ │ │ └── global/ │ │ │ │ │ │ ├── [bodyParser,notFound]buildQuery[graphql].js │ │ │ │ │ │ ├── [buildQuery]graphql[response].js │ │ │ │ │ │ └── bodyParser[buildQuery].ts │ │ │ │ │ └── services/ │ │ │ │ │ ├── buildResolvers.js │ │ │ │ │ ├── buildSchema.js │ │ │ │ │ ├── buildStoreFrontSchema.js │ │ │ │ │ ├── buildTypes.js │ │ │ │ │ ├── contextHelper.ts │ │ │ │ │ ├── graphqlErrorMessageFormat.js │ │ │ │ │ ├── graphqlMiddleware.js │ │ │ │ │ └── index.ts │ │ │ │ ├── oms/ │ │ │ │ │ ├── api/ │ │ │ │ │ │ ├── cancelOrder/ │ │ │ │ │ │ │ ├── [context]borderParser[auth].js │ │ │ │ │ │ │ ├── cancelOrder.js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── createShipment/ │ │ │ │ │ │ │ ├── [context]borderParser[auth].ts │ │ │ │ │ │ │ ├── createShipment.ts │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── lifetimesales/ │ │ │ │ │ │ │ ├── loadData.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── markDelivered/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ │ ├── markDelivered.ts │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── salestatistic/ │ │ │ │ │ │ │ ├── loadData.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ └── updateShipment/ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ └── updateShipment.ts │ │ │ │ │ ├── bootstrap.ts │ │ │ │ │ ├── graphql/ │ │ │ │ │ │ └── types/ │ │ │ │ │ │ ├── BestSeller/ │ │ │ │ │ │ │ ├── BestSeller.admin.graphql │ │ │ │ │ │ │ └── BestSeller.admin.resolvers.js │ │ │ │ │ │ ├── Carrier/ │ │ │ │ │ │ │ ├── Carrier.admin.graphql │ │ │ │ │ │ │ └── Carrier.admin.resolvers.js │ │ │ │ │ │ ├── Order/ │ │ │ │ │ │ │ ├── Order.admin.graphql │ │ │ │ │ │ │ ├── Order.admin.resolvers.js │ │ │ │ │ │ │ ├── Order.graphql │ │ │ │ │ │ │ └── Order.resolvers.js │ │ │ │ │ │ ├── PaymentTransaction/ │ │ │ │ │ │ │ ├── PaymentTransaction.admin.graphql │ │ │ │ │ │ │ └── PaymentTransaction.admin.resolvers.js │ │ │ │ │ │ └── Status/ │ │ │ │ │ │ ├── Status.graphql │ │ │ │ │ │ └── Status.resolvers.js │ │ │ │ │ ├── migration/ │ │ │ │ │ │ ├── Version-1.0.0.js │ │ │ │ │ │ ├── Version-1.0.1.js │ │ │ │ │ │ └── Version-1.0.2.ts │ │ │ │ │ ├── pages/ │ │ │ │ │ │ └── admin/ │ │ │ │ │ │ ├── all/ │ │ │ │ │ │ │ └── OmsMenuGroup.jsx │ │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ │ ├── Bestcustomers.jsx │ │ │ │ │ │ │ ├── Bestsellers.scss │ │ │ │ │ │ │ ├── Bestsellers.tsx │ │ │ │ │ │ │ ├── Lifetimesales.jsx │ │ │ │ │ │ │ ├── Lifetimesales.scss │ │ │ │ │ │ │ ├── Statistic.jsx │ │ │ │ │ │ │ └── Statistic.scss │ │ │ │ │ │ ├── orderEdit/ │ │ │ │ │ │ │ ├── Activities.jsx │ │ │ │ │ │ │ ├── AddTrackingButton.tsx │ │ │ │ │ │ │ ├── CancelButton.tsx │ │ │ │ │ │ │ ├── Customer.tsx │ │ │ │ │ │ │ ├── CustomerNotes.tsx │ │ │ │ │ │ │ ├── Items.jsx │ │ │ │ │ │ │ ├── Layout.jsx │ │ │ │ │ │ │ ├── Layout.scss │ │ │ │ │ │ │ ├── MarkDeliveredButton.tsx │ │ │ │ │ │ │ ├── PageHeading.tsx │ │ │ │ │ │ │ ├── Payment.jsx │ │ │ │ │ │ │ ├── ShipButton.tsx │ │ │ │ │ │ │ ├── Status.jsx │ │ │ │ │ │ │ ├── TrackingButton.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── items/ │ │ │ │ │ │ │ │ ├── ItemVariantOptions.tsx │ │ │ │ │ │ │ │ ├── Name.tsx │ │ │ │ │ │ │ │ ├── Price.tsx │ │ │ │ │ │ │ │ └── Thumbnail.tsx │ │ │ │ │ │ │ ├── payment/ │ │ │ │ │ │ │ │ ├── Discount.tsx │ │ │ │ │ │ │ │ ├── Shipping.tsx │ │ │ │ │ │ │ │ ├── SubTotal.tsx │ │ │ │ │ │ │ │ ├── Tax.tsx │ │ │ │ │ │ │ │ └── Total.tsx │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ └── orderGrid/ │ │ │ │ │ │ ├── Grid.jsx │ │ │ │ │ │ ├── Heading.tsx │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ └── rows/ │ │ │ │ │ │ ├── OrderNumber.tsx │ │ │ │ │ │ ├── PaymentStatus.tsx │ │ │ │ │ │ └── ShipmentStatus.tsx │ │ │ │ │ ├── services/ │ │ │ │ │ │ ├── OrderCollection.js │ │ │ │ │ │ ├── addOrderActivityLog.ts │ │ │ │ │ │ ├── cancelOrder.ts │ │ │ │ │ │ ├── createShipment.ts │ │ │ │ │ │ ├── getOrdersBaseQuery.js │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── registerDefaultOrderCollectionFilters.js │ │ │ │ │ │ ├── updateOrderStatus.ts │ │ │ │ │ │ ├── updatePaymentStatus.ts │ │ │ │ │ │ └── updateShipmentStatus.ts │ │ │ │ │ └── subscribers/ │ │ │ │ │ └── order_placed/ │ │ │ │ │ └── sendOrderConfirmationEmail.ts │ │ │ │ ├── paypal/ │ │ │ │ │ ├── api/ │ │ │ │ │ │ ├── paypalAuthorizePayment/ │ │ │ │ │ │ │ ├── [bodyParser]authorize.ts │ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── paypalCaptureAuthorizedPayment/ │ │ │ │ │ │ │ ├── [bodyParser]capture.ts │ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── paypalCapturePayment/ │ │ │ │ │ │ │ ├── [bodyParser]capture.ts │ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ └── paypalCreateOrder/ │ │ │ │ │ │ ├── [bodyParser]createOrder.ts │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ └── route.json │ │ │ │ │ ├── bootstrap.js │ │ │ │ │ ├── graphql/ │ │ │ │ │ │ └── types/ │ │ │ │ │ │ └── PaypalSetting/ │ │ │ │ │ │ ├── PaypalSetting.admin.graphql │ │ │ │ │ │ ├── PaypalSetting.admin.resolvers.js │ │ │ │ │ │ ├── PaypalSetting.graphql │ │ │ │ │ │ └── PaypalSetting.resolvers.js │ │ │ │ │ ├── pages/ │ │ │ │ │ │ ├── admin/ │ │ │ │ │ │ │ ├── orderEdit/ │ │ │ │ │ │ │ │ └── PaypalCaptureButton.tsx │ │ │ │ │ │ │ └── paymentSetting/ │ │ │ │ │ │ │ └── PaypalSetting.tsx │ │ │ │ │ │ └── frontStore/ │ │ │ │ │ │ ├── checkout/ │ │ │ │ │ │ │ └── Paypal.tsx │ │ │ │ │ │ ├── paypalCancel/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ └── paypalReturn/ │ │ │ │ │ │ ├── Error.tsx │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── route.json │ │ │ │ │ └── services/ │ │ │ │ │ ├── getApiBaseUrl.js │ │ │ │ │ ├── requester.js │ │ │ │ │ └── voidPaymentTransaction.js │ │ │ │ ├── promotion/ │ │ │ │ │ ├── api/ │ │ │ │ │ │ ├── couponApply/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ │ ├── [validateCouponCode]applyCoupon.ts │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── validateCouponCode.js │ │ │ │ │ │ ├── couponRemove/ │ │ │ │ │ │ │ ├── removeCoupon.ts │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── createCoupon/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── createCoupon[finish].js │ │ │ │ │ │ │ ├── finish[apiResponse].js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── deleteCoupon/ │ │ │ │ │ │ │ ├── deleteCoupon.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ └── updateCoupon/ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ ├── finish[apiResponse].js │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ └── updateCoupon[finish].js │ │ │ │ │ ├── bootstrap.js │ │ │ │ │ ├── graphql/ │ │ │ │ │ │ └── types/ │ │ │ │ │ │ └── Coupon/ │ │ │ │ │ │ ├── Coupon.admin.graphql │ │ │ │ │ │ ├── Coupon.admin.resolvers.js │ │ │ │ │ │ ├── Coupon.graphql │ │ │ │ │ │ └── Coupon.resolvers.js │ │ │ │ │ ├── migration/ │ │ │ │ │ │ ├── Version-1.0.0.js │ │ │ │ │ │ └── Version-1.0.1.js │ │ │ │ │ ├── pages/ │ │ │ │ │ │ └── admin/ │ │ │ │ │ │ ├── all/ │ │ │ │ │ │ │ ├── CouponMenuGroup.tsx │ │ │ │ │ │ │ └── NewCouponQuickLink.tsx │ │ │ │ │ │ ├── couponEdit/ │ │ │ │ │ │ │ ├── CouponEditForm.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── couponEdit+couponNew/ │ │ │ │ │ │ │ ├── CustomerCondition.tsx │ │ │ │ │ │ │ ├── DiscountType.tsx │ │ │ │ │ │ │ ├── General.scss │ │ │ │ │ │ │ ├── General.tsx │ │ │ │ │ │ │ ├── OrderCondition.tsx │ │ │ │ │ │ │ ├── PageHeading.tsx │ │ │ │ │ │ │ └── components/ │ │ │ │ │ │ │ ├── AttributeGroupConditionValueSelector.tsx │ │ │ │ │ │ │ ├── BuyXGetY.tsx │ │ │ │ │ │ │ ├── CategoryConditionValueSelector.tsx │ │ │ │ │ │ │ ├── CollectionConditionValueSelector.tsx │ │ │ │ │ │ │ ├── PriceConditionValueSelector.tsx │ │ │ │ │ │ │ ├── RequireProducts.tsx │ │ │ │ │ │ │ ├── Setting.tsx │ │ │ │ │ │ │ ├── SkuConditionValueSelector.tsx │ │ │ │ │ │ │ ├── TargetProducts.tsx │ │ │ │ │ │ │ ├── ValueSelector.tsx │ │ │ │ │ │ │ └── conditionCriterias.ts │ │ │ │ │ │ ├── couponGrid/ │ │ │ │ │ │ │ ├── Grid.jsx │ │ │ │ │ │ │ ├── Heading.tsx │ │ │ │ │ │ │ ├── NewCouponButton.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ │ └── rows/ │ │ │ │ │ │ │ └── CouponName.tsx │ │ │ │ │ │ ├── couponNew/ │ │ │ │ │ │ │ ├── CouponNewForm.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ └── navigation/ │ │ │ │ │ │ ├── CouponNewMenuItem.tsx │ │ │ │ │ │ └── CouponsMenuItem.tsx │ │ │ │ │ └── services/ │ │ │ │ │ ├── CouponCollection.js │ │ │ │ │ ├── coupon/ │ │ │ │ │ │ ├── couponDataSchema.json │ │ │ │ │ │ ├── createCoupon.js │ │ │ │ │ │ ├── deleteCoupon.js │ │ │ │ │ │ └── updateCoupon.js │ │ │ │ │ ├── couponValidator.js │ │ │ │ │ ├── discountCalculator.js │ │ │ │ │ ├── getCartTotalBeforeDiscount.js │ │ │ │ │ ├── getCouponsBaseQuery.js │ │ │ │ │ ├── registerCartItemPromotionFields.js │ │ │ │ │ ├── registerCartPromotionFields.js │ │ │ │ │ ├── registerDefaultCalculators.js │ │ │ │ │ ├── registerDefaultCouponCollectionFilters.js │ │ │ │ │ └── registerDefaultValidators.js │ │ │ │ ├── setting/ │ │ │ │ │ ├── api/ │ │ │ │ │ │ └── saveSetting/ │ │ │ │ │ │ ├── [context]bodyParser[auth].ts │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ └── saveSetting.js │ │ │ │ │ ├── graphql/ │ │ │ │ │ │ └── types/ │ │ │ │ │ │ ├── Setting/ │ │ │ │ │ │ │ ├── Setting.graphql │ │ │ │ │ │ │ └── Setting.resolvers.js │ │ │ │ │ │ ├── ShippingSetting/ │ │ │ │ │ │ │ ├── ShippingSetting.graphql │ │ │ │ │ │ │ └── ShippingSetting.resolvers.js │ │ │ │ │ │ └── StoreSetting/ │ │ │ │ │ │ ├── StoreSetting.graphql │ │ │ │ │ │ └── StoreSetting.resolvers.js │ │ │ │ │ ├── migration/ │ │ │ │ │ │ └── Version-1.0.0.js │ │ │ │ │ ├── pages/ │ │ │ │ │ │ └── admin/ │ │ │ │ │ │ ├── all/ │ │ │ │ │ │ │ ├── PaymentSettingMenu.tsx │ │ │ │ │ │ │ ├── SettingMenuGroup.tsx │ │ │ │ │ │ │ └── StoreSettingMenu.tsx │ │ │ │ │ │ ├── paymentSetting/ │ │ │ │ │ │ │ ├── PaymentSetting.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ └── storeSetting/ │ │ │ │ │ │ ├── StoreSetting.tsx │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── route.json │ │ │ │ │ └── services/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── setting.ts │ │ │ │ ├── stripe/ │ │ │ │ │ ├── api/ │ │ │ │ │ │ ├── capturePaymentIntent/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── capturePaymentIntent.js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── createPaymentIntent/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── createPaymentIntent.js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ ├── refundPaymentIntent/ │ │ │ │ │ │ │ ├── [context]bodyParser[auth].js │ │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ │ ├── refundPaymentIntent.js │ │ │ │ │ │ │ └── route.json │ │ │ │ │ │ └── stripeWebHook/ │ │ │ │ │ │ ├── [bodyJson]webhook.js │ │ │ │ │ │ ├── bodyJson.js │ │ │ │ │ │ └── route.json │ │ │ │ │ ├── bootstrap.js │ │ │ │ │ ├── graphql/ │ │ │ │ │ │ └── types/ │ │ │ │ │ │ └── StripeSetting/ │ │ │ │ │ │ ├── StripeSetting.admin.graphql │ │ │ │ │ │ ├── StripeSetting.admin.resolvers.js │ │ │ │ │ │ ├── StripeSetting.graphql │ │ │ │ │ │ └── StripeSetting.resolvers.js │ │ │ │ │ ├── pages/ │ │ │ │ │ │ ├── admin/ │ │ │ │ │ │ │ ├── orderEdit/ │ │ │ │ │ │ │ │ ├── StripeCaptureButton.jsx │ │ │ │ │ │ │ │ └── StripeRefundButton.tsx │ │ │ │ │ │ │ └── paymentSetting/ │ │ │ │ │ │ │ └── StripePayment.tsx │ │ │ │ │ │ └── frontStore/ │ │ │ │ │ │ ├── checkout/ │ │ │ │ │ │ │ └── Stripe.tsx │ │ │ │ │ │ └── stripeReturn/ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── route.json │ │ │ │ │ └── services/ │ │ │ │ │ └── cancelPayment.js │ │ │ │ └── tax/ │ │ │ │ ├── api/ │ │ │ │ │ ├── createTaxClass/ │ │ │ │ │ │ ├── [context]borderParser[auth].ts │ │ │ │ │ │ ├── createTaxClass.ts │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ └── route.json │ │ │ │ │ ├── createTaxRate/ │ │ │ │ │ │ ├── [context]borderParser[auth].ts │ │ │ │ │ │ ├── createTaxRate.ts │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ └── route.json │ │ │ │ │ ├── deleteTaxRate/ │ │ │ │ │ │ ├── [context]borderParser[auth].ts │ │ │ │ │ │ ├── deleteTaxRate.ts │ │ │ │ │ │ └── route.json │ │ │ │ │ ├── updateTaxClass/ │ │ │ │ │ │ ├── [context]borderParser[auth].js │ │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ │ ├── route.json │ │ │ │ │ │ └── updateTaxClass.js │ │ │ │ │ └── updateTaxRate/ │ │ │ │ │ ├── [context]borderParser[auth].js │ │ │ │ │ ├── payloadSchema.json │ │ │ │ │ ├── route.json │ │ │ │ │ └── updateTaxRate.js │ │ │ │ ├── bootstrap.js │ │ │ │ ├── graphql/ │ │ │ │ │ └── types/ │ │ │ │ │ ├── Product/ │ │ │ │ │ │ └── Price/ │ │ │ │ │ │ └── ProductPrice.resolvers.js │ │ │ │ │ ├── TaxClass/ │ │ │ │ │ │ ├── TaxClass.admin.graphql │ │ │ │ │ │ └── TaxClass.admin.resolvers.js │ │ │ │ │ └── TaxSetting/ │ │ │ │ │ ├── TaxSetting.admin.graphql │ │ │ │ │ ├── TaxSetting.admin.resolvers.js │ │ │ │ │ ├── TaxSetting.graphql │ │ │ │ │ └── TaxSetting.resolvers.js │ │ │ │ ├── migration/ │ │ │ │ │ └── Version-1.0.0.js │ │ │ │ ├── pages/ │ │ │ │ │ └── admin/ │ │ │ │ │ ├── all/ │ │ │ │ │ │ └── TaxSettingMenu.tsx │ │ │ │ │ └── taxSetting/ │ │ │ │ │ ├── TaxSetting.tsx │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── Rate.tsx │ │ │ │ │ │ ├── RateForm.tsx │ │ │ │ │ │ ├── Rates.tsx │ │ │ │ │ │ ├── TaxClass.tsx │ │ │ │ │ │ ├── TaxClassForm.tsx │ │ │ │ │ │ └── TaxClasses.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── route.json │ │ │ │ └── services/ │ │ │ │ ├── TaxClassCollection.js │ │ │ │ ├── calculateTaxAmount.js │ │ │ │ ├── getTaxPercent.js │ │ │ │ ├── getTaxRates.js │ │ │ │ ├── registerCartItemTaxPercentField.js │ │ │ │ └── registerDefaultTaxClassCollectionFilters.js │ │ │ └── types/ │ │ │ ├── apiResponse.ts │ │ │ ├── appContext.tsx │ │ │ ├── atLeastOne.ts │ │ │ ├── checkoutData.ts │ │ │ ├── componentLayout.ts │ │ │ ├── cronjob.ts │ │ │ ├── customerAddress.ts │ │ │ ├── db/ │ │ │ │ └── index.ts │ │ │ ├── event.ts │ │ │ ├── extension.ts │ │ │ ├── graphqlFilter.ts │ │ │ ├── middleware.ts │ │ │ ├── order.ts │ │ │ ├── pageMeta.ts │ │ │ ├── request.d.ts │ │ │ ├── request.ts │ │ │ ├── response.ts │ │ │ ├── route.ts │ │ │ └── widget.ts │ │ └── tsconfig.json │ └── postgres-query-builder/ │ ├── .swcrc │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src/ │ │ ├── fieldResolve.js │ │ ├── index.ts │ │ ├── isValueASQL.js │ │ └── toString.js │ └── tsconfig.json ├── translations/ │ ├── de/ │ │ ├── account.csv │ │ ├── catalog.csv │ │ ├── checkout.csv │ │ ├── general.csv │ │ └── paypal.csv │ ├── es/ │ │ ├── account.csv │ │ ├── catalog.csv │ │ ├── checkout.csv │ │ ├── general.csv │ │ └── paypal.csv │ ├── fa/ │ │ ├── account.csv │ │ ├── catalog.csv │ │ ├── checkout.csv │ │ ├── general.csv │ │ └── paypal.csv │ ├── fr/ │ │ ├── account.csv │ │ ├── catalog.csv │ │ ├── checkout.csv │ │ ├── general.csv │ │ └── paypal.csv │ ├── gr/ │ │ ├── account.csv │ │ ├── catalog.csv │ │ ├── checkout.csv │ │ ├── general.csv │ │ └── paypal.csv │ ├── hu/ │ │ ├── account.csv │ │ ├── catalog.csv │ │ ├── checkout.csv │ │ └── general.csv │ ├── it/ │ │ ├── account.csv │ │ ├── catalog.csv │ │ ├── checkout.csv │ │ ├── general.csv │ │ └── paypal.csv │ ├── mn/ │ │ ├── account.csv │ │ ├── catalog.csv │ │ ├── checkout.csv │ │ ├── general.csv │ │ └── paypal.csv │ ├── nb/ │ │ ├── account.csv │ │ ├── catalog.csv │ │ ├── checkout.csv │ │ ├── general.csv │ │ └── paypal.csv │ ├── ne/ │ │ ├── account.csv │ │ ├── catalog.csv │ │ ├── checkout.csv │ │ ├── general.csv │ │ └── paypal.csv │ ├── nl/ │ │ ├── account.csv │ │ ├── catalog.csv │ │ ├── checkout.csv │ │ ├── general.csv │ │ └── paypal.csv │ ├── pt/ │ │ ├── account.csv │ │ ├── catalog.csv │ │ ├── checkout.csv │ │ ├── general.csv │ │ └── paypal.csv │ ├── rs/ │ │ ├── account.csv │ │ ├── catalog.csv │ │ ├── checkout.csv │ │ ├── general.csv │ │ └── paypal.csv │ ├── ru/ │ │ ├── account.csv │ │ ├── catalog.csv │ │ ├── checkout.csv │ │ ├── general.csv │ │ └── paypal.csv │ ├── ta/ │ │ ├── account.csv │ │ ├── catalog.csv │ │ ├── checkout.csv │ │ ├── general.csv │ │ └── paypal.csv │ ├── vn/ │ │ ├── account.csv │ │ ├── catalog.csv │ │ ├── checkout.csv │ │ ├── general.csv │ │ └── paypal.csv │ └── zh/ │ ├── account.csv │ ├── catalog.csv │ ├── checkout.csv │ ├── general.csv │ └── paypal.csv └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms open_collective: evershopcommerce ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: "[BUG]" labels: '' assignees: treoden --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Background (please complete the following information):** - NodeJS Version - Postgres Version - EverShop Version - OS: [e.g. Window, Ubuntu, Mac-OS] - Browser [e.g. Chrome, Safari] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: "[FEATURE REQUEST]" labels: '' assignees: treoden --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/pull_request_template.md ================================================ ## PR Checklist Please check if your PR fulfills the following requirements: - [ ] Tests for the changes have been added (for bug fixes / features) ## PR Type What kind of change does this PR introduce? - [ ] Bugfix - [ ] Feature - [ ] Code style update (formatting, local variables) - [ ] Refactoring (no functional changes, no api changes) - [ ] Build related changes - [ ] CI related changes - [ ] Other... Please describe: ## What is the current behavior? Issue Number: N/A ## What is the new behavior? ## Does this PR introduce a breaking change? - [ ] Yes - [ ] No ## Other information ================================================ FILE: .github/workflows/build_test.yml ================================================ name: Github build on: [pull_request] jobs: build: runs-on: ubuntu-latest strategy: matrix: node: [ '20', '22'] name: Node ${{ matrix.node }} steps: - uses: actions/checkout@v2 - name: Setup node js uses: actions/setup-node@v2 with: node-version: ${{ matrix.node }} - run: npm install -g npm@9 - run: npm install - run: npm run compile - run: npm run compile:db - run: npm run test ================================================ FILE: .gitignore ================================================ node_modules .env dist !packages/create-evershop-app/sample/extensions/sample/dist !packages/create-evershop-app/sample/themes/sample/dist /extensions/* !extensions/agegate !extensions/azure_file_storage !extensions/google_login !extensions/s3_file_storage !extensions/resend !extensions/sendgrid !extensions/product_review /themes .idea/* .evershop .vscode npm-debug.log /package-log.json /media /public .DS_Store .log coverage cypress .turbo /config /packages/evershop/config .prettierignore .prettierrc mysqlToPostgres.js /docs extensions ================================================ FILE: .husky/pre-commit ================================================ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" #FORCE_COLOR=1 npm run lint ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [support@evershop.io](mailto:support@evershop.io). All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. # License EverShop is licensed under the GNU General Public License v3.0 ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to EverShop We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: - Reporting a bug - Discussing the current state of the code - Submitting a fix - Proposing new features --- - Read about our [Code Of Conduct](https://github.com/evershopcommerce/evershop/blob/main/CODE_OF_CONDUCT.md). ## Developing To develop locally: 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device. ```sh git clone https://github.com/evershopcommerce/evershop.git ``` 2. Create a new branch: ``` git checkout -b MY_BRANCH_NAME ``` 3. Install the dependencies with: ``` npm install ``` 4. Create a Postgres database: ``` // EverShop use Postgres for database storage ``` 5. Run installation command to create a database schema: ``` npm run setup ``` 6. Start development server: ``` npm run dev ``` 7. Building You can build with: ```bash npm run build ``` 8. Testing the production build ```bash npm run start ``` 9. Running tests Run the [Jest](https://jestjs.io/) unit testing ```sh npm run test ``` 10. Running linting ```sh npm run lint ``` 11. Issue that [pull request!](https://github.com/github/docs/blob/main/CONTRIBUTING.md) to the `dev` branch. ## Any contributions you make will be under the GNU General Public License v3.0 Software License In short, when you submit code changes, your submissions are understood to be under the same [GNU General Public License v3.0](https://github.com/evershopcommerce/evershop/blob/main/LICENSE) that covers the project. Feel free to contact the maintainers if that's a concern. ## Report bugs using Github's [issues](https://github.com/evershopcommerce/evershop/issues) We use GitHub issues to track public bugs. Report a bug by [opening a new issue](); it's that easy! ## Write bug reports with detail, background, and sample code **Great Bug Reports** tend to have: - A quick summary and/or background - What EverShop version you are using - What NodeJs version you are using - What OS system you are using - What Postgres version you are using - Steps to reproduce - Be specific! - Give sample code if you can - What you expected would happen - What actually happens - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) ## License By contributing, you agree that your contributions will be licensed under its GNU General Public License v3.0 License. ================================================ FILE: Dockerfile ================================================ FROM node:18-alpine WORKDIR /app RUN npm install -g npm@9 COPY package*.json . COPY packages ./packages COPY themes ./themes COPY extensions ./extensions COPY public ./public COPY media ./media COPY config ./config COPY translations ./translations RUN npm install RUN npm run build EXPOSE 80 CMD ["npm", "run", "start"] ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================

      

EverShop Logo

EverShop

Documentation | Demo

Github Action Twitter Follow Discord License

EverShop

## Introduction EverShop is a modern, TypeScript-first eCommerce platform built with GraphQL and React. Designed for developers, it offers essential commerce features in a modular, fully customizable architecture—perfect for building tailored shopping experiences with confidence and speed. ## Installation Using Docker You can get started with EverShop in minutes by using the Docker image. The Docker image is a great way to get started with EverShop without having to worry about installing dependencies or configuring your environment. ```bash curl -sSL https://raw.githubusercontent.com/evershopcommerce/evershop/main/docker-compose.yml > docker-compose.yml docker compose up -d ``` For the full installation guide, please refer to our [Installation guide](https://evershop.io/docs/development/getting-started/installation-guide). ## Documentation - [Installation guide](https://evershop.io/docs/development/getting-started/installation-guide). - [Extension development](https://evershop.io/docs/development/module/create-your-first-extension). - [Theme development](https://evershop.io/docs/development/theme/theme-overview). ## Demo Explore our demo store.

evershop-backend-demo evershop-store-demo

Demo user: Email: demo@evershop.io
Password: 123456 ## Support If you like my work, feel free to: - ⭐ this repository. It helps. - [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)][tweet] about EverShop. Thank you! [tweet]: https://twitter.com/intent/tweet?url=https%3A%2F%2Fgithub.com%2Fevershopcommerce%2Fevershop&text=Awesome%20React%20Ecommerce%20Project&hashtags=react,ecommerce,expressjs,graphql ## Contributing EverShop is an open-source project. We are committed to a fully transparent development process and appreciate highly any contributions. Whether you are helping us fix bugs, proposing new features, improving our documentation or spreading the word - we would love to have you as part of the EverShop community. ### Ask a question about EverShop You can ask questions, and participate in discussions about EverShop-related topics in the EverShop Discord channel. ### Create a bug report If you see an error message or run into an issue, please [create bug report](https://github.com/evershopcommerce/evershop/issues/new). This effort is valued and it will help all EverShop users. ### Submit a feature request If you have an idea, or you're missing a capability that would make development easier and more robust, please [Submit feature request](https://github.com/evershopcommerce/evershop/issues/new). If a similar feature request already exists, don't forget to leave a "+1". If you add some more information such as your thoughts and vision about the feature, your comments will be embraced warmly :) Please refer to our [Contribution Guidelines](./CONTRIBUTING.md) and [Code of Conduct](./CODE_OF_CONDUCT.md). ## 🚀 The Future of EverShop EverShop is seeing rapid organic growth and strong adoption from the developer community. We are now scaling our operations and building **EverShop Cloud**. If you are a strategic investor interested in the future of Node.js commerce and our mission to set a new standard for modern eCommerce, we’d love to share our vision and roadmap with you. 📩 **Get in touch:** support@evershop.io ## License [GPL-3.0 License](https://github.com/evershopcommerce/evershop/blob/main/LICENSE) ================================================ FILE: docker-compose.yml ================================================ version: '3.8' services: app: image: evershop/evershop:latest restart: always environment: DB_HOST: database DB_PORT: 5432 DB_PASSWORD: postgres DB_USER: postgres DB_NAME: postgres networks: - myevershop depends_on: - database ports: - 3000:3000 #The postgres database: database: image: postgres:16 restart: unless-stopped volumes: - postgres-data:/var/lib/postgresql/data environment: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres POSTGRES_DB: postgres ports: - "5432:5432" networks: - myevershop networks: myevershop: name: MyEverShop driver: bridge volumes: postgres-data: ================================================ FILE: eslint.config.js ================================================ // eslint.config.js import eslintPluginTypescript from '@typescript-eslint/eslint-plugin'; import typescriptParser from '@typescript-eslint/parser'; import pluginImport from 'eslint-plugin-import'; import jsxA11y from 'eslint-plugin-jsx-a11y'; import pluginReact from 'eslint-plugin-react'; export default [ { ignores: [ "/node_modules/", "**/*test.js", "**/tests/**", "**/create-evershop-app/**", "**/.evershop/**", "/.vscode/**", "/.git/**", "/.idea/**", "**/extensions/**", "**/public/**", "**/themes/**", "**/media/**", "**/dist/**", "**/packages/*/dist/**", "**/packages/evershop/dist/**", "**/packages/postgres-query-builder/dist/**", "**/packages/product_review/**", "**/packages/resend/**" ]}, pluginReact.configs.flat.recommended, pluginReact.configs.flat['jsx-runtime'], jsxA11y.flatConfigs.recommended, { files: ["**/*.ts", "**/*.tsx", "**/*.d.ts"], plugins: { "@typescript-eslint": eslintPluginTypescript }, languageOptions: { parser: typescriptParser, parserOptions: { ecmaVersion: 'latest', sourceType: 'module' } } }, { plugins: { "import": pluginImport }, languageOptions: { ecmaVersion: 'latest', sourceType: 'module', parserOptions : { ecmaFeatures: { jsx: true } } }, rules: { semi: "off", "prefer-const": "error", "import/no-dynamic-require": 0, "no-else-return": "off", "import/prefer-default-export": 0, "jsx-a11y/anchor-is-valid": 0, "import/no-extraneous-dependencies": "off", "import/no-unresolved": "off", "camelcase": "off", "no-multi-assign": "off", "no-template-curly-in-string": "off", "react/no-array-index-key": "off", "react/no-unstable-nested-components": "off", "no-continue": "off", "no-await-in-loop": "off", "no-use-before-define" : "off", "global-require": "off", "import/extensions": "off", "no-shadow": "off", "no-lonely-if": "warn", "no-console": "error", "no-useless-return": "off", "react/display-name": "off", "jsx-a11y/label-has-associated-control": "off", "jsx-a11y/role-supports-aria-props": "warn", "jsx-a11y/no-noninteractive-element-interactions": "warn", // Add import sorting rules "import/order": ["warn", { "groups": ["builtin", "external", "internal", "parent", "sibling", "index"], "newlines-between": "never", "alphabetize": { "order": "asc", "caseInsensitive": true } }] }, settings: { react: { version: "detect" } } } ] ================================================ FILE: jest.config.js ================================================ export default { testEnvironment: "node", moduleNameMapper: { '^@evershop/postgres-query-builder$': '/packages/postgres-query-builder/dist/index.js', '^@evershop/postgres-query-builder/(.*)$': '/packages/postgres-query-builder/dist/$1', '^(\\.{1,2}/.*)\\.js$': '$1' }, transformIgnorePatterns: [ "/node_modules/(?!(@evershop)/)" ], testMatch: ["**/dist/**/tests/**/unit/**/*.test.[jt]s"], modulePathIgnorePatterns: ["/packages/evershop/src/"] }; ================================================ FILE: package.json ================================================ { "name": "evershop", "version": "2.1.0", "type": "module", "description": "A shopping cart platform with Express, React and Postgres", "workspaces": [ "packages/*", "extensions/*" ], "scripts": { "dev": "node ./packages/evershop/dist/bin/dev/index.js", "start": "node ./packages/evershop/dist/bin/start/index.js", "build": "node ./packages/evershop/dist/bin/build/index.js", "build-fast": "evershop build -- --skip-minify", "setup": "evershop install", "theme:active": "evershop theme:active", "theme:twizz": "evershop theme:twizz", "theme:create": "evershop theme:create", "compile": "rimraf ./packages/evershop/dist && cd ./packages/evershop && swc ./src/ -d dist/ --config-file .swcrc --copy-files --strip-leading-paths", "compile:db": "rimraf ./packages/postgres-query-builder/dist && cd ./packages/postgres-query-builder && swc ./src/ -d dist/ --config-file .swcrc --copy-files --strip-leading-paths", "compile:tsc": "rimraf ./packages/evershop/dist && cd ./packages/evershop && tsc && copyfiles -u 1 \"src/**/*.{graphql,scss,css,json}\" dist", "start:debug": "node ./packages/evershop/dist/bin/start/index.js --debug", "test": "ALLOW_CONFIG_MUTATIONS=true NODE_OPTIONS=--experimental-vm-modules node_modules/jest/bin/jest.js", "lint": "eslint --fix --ext .js,.jsx,.ts,.tsx ./packages", "prepare": "husky install" }, "author": "The Nguyen (https://evershop.io)", "license": "GNU GENERAL PUBLIC LICENSE 3.0", "devDependencies": { "@parcel/watcher": "^2.5.1", "@swc/cli": "^0.7.7", "@swc/core": "^1.11.29", "@types/jest": "^29.5.14", "@types/jsonwebtoken": "^9.0.10", "@typescript-eslint/eslint-plugin": "^8.32.0", "@typescript-eslint/parser": "^8.32.0", "copyfiles": "^2.4.1", "cypress": "^13.15.1", "eslint": "^9.24.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react": "^7.37.5", "execa": "^9.6.0", "husky": "^8.0.3", "jest": "^29.7.0", "prettier": "2.8.4", "reflect-metadata": "^0.1.13", "rimraf": "^6.0.1", "swc-minify-webpack-plugin": "^2.1.3", "tailwindcss": "^4.1.18", "typescript": "^5.8.3", "webpack-bundle-analyzer": "^4.10.2" }, "dependencies": { "@sendgrid/mail": "^8.1.6", "@types/react-slick": "^0.23.13", "@types/uuid": "^10.0.0", "uuid": "^13.0.0" } } ================================================ FILE: packages/create-evershop-app/README.md ================================================ # create-evershop-app This package includes the global command for [Create EverShop App](https://evershop.io/).
Please refer to its documentation: - [Getting Started](https://evershop.io/docs/development/getting-started/introduction) – How to create a new app. - [Development Guide](https://evershop.io/docs/development/) – How to develop an ecommerce web app with EverShop. ================================================ FILE: packages/create-evershop-app/createEverShopApp.js ================================================ const https = require('https'); const chalk = require('chalk'); const commander = require('commander'); const dns = require('dns'); const { execSync } = require('child_process'); const fs = require('fs-extra'); const os = require('os'); const path = require('path'); const semver = require('semver'); const spawn = require('cross-spawn'); const url = require('url'); const validateProjectName = require('validate-npm-package-name'); const { mkdir } = require('fs/promises'); const packageJson = require('./package.json'); function isUsingYarn() { return (process.env.npm_config_user_agent || '').indexOf('yarn') === 0; } let projectName; function init() { const program = new commander.Command(packageJson.name) .version(packageJson.version) .arguments('[project-directory]') .usage(`${chalk.green('')} [options]`) .action((name) => { projectName = name; }) .option('--verbose', 'Print additional logs') .option('--info', 'Print environment debug info') .on('--help', () => { console.log( ` Only ${chalk.green('')} is required.` ); console.log(); console.log( ` If you have any problems, do not hesitate to file an issue:` ); console.log( ` ${chalk.cyan( 'https://github.com/evershop/create-evershop-app/issues/new' )}` ); console.log(); }) .parse(process.argv); const options = program.opts(); if (typeof projectName === 'undefined') { console.error('Please specify the project directory:'); console.log( ` ${chalk.cyan(program.name())} ${chalk.green('')}` ); console.log(); console.log('For example:'); console.log( ` ${chalk.cyan(program.name())} ${chalk.green('my-evershop-app')}` ); console.log(); console.log( `Run ${chalk.cyan(`${program.name()} --help`)} to see all options.` ); process.exit(1); } // We first check the registry directly via the API, and if that fails, we try // the slower `npm view [package] version` command. // // This is important for users in environments where direct access to npm is // blocked by a firewall, and packages are provided exclusively via a private // registry. checkForLatestVersion() .catch(() => { try { return execSync('npm view create-evershop-app version') .toString() .trim(); } catch (e) { return null; } }) .then((latest) => { if (latest && semver.lt(packageJson.version, latest)) { console.log(); console.error( chalk.yellow( `You are running \`create-evershop-app\` ${packageJson.version}, which is behind the latest release (${latest}).\n\n` + 'We recommend always using the latest version of create-evershop-app if possible.' ) ); console.log(); console.log( 'The latest instructions for creating a new app can be found here:\n' + 'https://evershop.io/docs/development/getting-started/installation-guide/' ); console.log(); } else { const useYarn = isUsingYarn(); createApp(projectName, options.verbose, useYarn); } }); } function createApp(name, verbose, useYarn) { const root = path.resolve(name); const appName = path.basename(root); checkAppName(appName); fs.ensureDirSync(name); if (!isSafeToCreateProjectIn(root, name)) { process.exit(1); } console.log(); console.log(`Creating a new EverShop app in ${chalk.green(root)}.`); console.log(); const packageJson = { name: appName, version: '0.1.0', type: 'module', private: true, scripts: { setup: 'evershop install', start: 'evershop start', build: 'evershop build', dev: 'evershop dev' } }; fs.writeFileSync( path.join(root, 'package.json'), JSON.stringify(packageJson, null, 2) + os.EOL ); const originalDirectory = process.cwd(); process.chdir(root); if (!useYarn && !checkThatNpmCanReadCwd()) { process.exit(1); } if (!useYarn) { const npmInfo = checkNpmVersion(); if (!npmInfo.hasMinNpm) { if (npmInfo.npmVersion) { console.log( chalk.yellow( `Please update to npm 7 or higher for a workspaces feature.\n` ) ); } } } run(root, appName, verbose, originalDirectory, useYarn); } function install(root, useYarn, dependencies, verbose, isOnline) { return new Promise((resolve, reject) => { let command; let args; if (useYarn) { command = 'yarnpkg'; args = ['add', '--exact']; if (!isOnline) { args.push('--offline'); } [].push.apply(args, dependencies); // Explicitly set cwd() to work around issues like // https://github.com/facebook/create-react-app/issues/3326. // Unfortunately we can only do this for Yarn because npm support for // equivalent --prefix flag doesn't help with this issue. // This is why for npm, we run checkThatNpmCanReadCwd() early instead. args.push('--cwd'); args.push(root); if (!isOnline) { console.log(chalk.yellow('You appear to be offline.')); console.log(chalk.yellow('Falling back to the local Yarn cache.')); console.log(); } } else { command = 'npm'; args = [ 'install', '--no-audit', // https://github.com/facebook/create-evershop-app/issues/11174 '--save', '--save-exact', '--loglevel', 'error' ].concat(dependencies); } if (verbose) { args.push('--verbose'); } const child = spawn(command, args, { stdio: 'inherit' }); child.on('close', (code) => { if (code !== 0) { reject({ command: `${command} ${args.join(' ')}` }); return; } resolve(); }); }); } function installDevDependencies( root, useYarn, devDependencies, verbose, isOnline ) { console.log(`Installing some development dependencies...`); return new Promise((resolve, reject) => { let command; let args; if (useYarn) { command = 'yarnpkg'; args = ['add', '--exact']; if (!isOnline) { args.push('--offline'); } [].push.apply(args, devDependencies); args.push('--dev'); // Explicitly set cwd() to work around issues like // https://github.com/facebook/create-react-app/issues/3326. // Unfortunately we can only do this for Yarn because npm support for // equivalent --prefix flag doesn't help with this issue. // This is why for npm, we run checkThatNpmCanReadCwd() early instead. args.push('--cwd'); args.push(root); if (!isOnline) { console.log(chalk.yellow('You appear to be offline.')); console.log(chalk.yellow('Falling back to the local Yarn cache.')); console.log(); } } else { command = 'npm'; args = [ 'install', '--no-audit', // https://github.com/facebook/create-evershop-app/issues/11174 '--save', '--save-exact', '--loglevel', 'error' ].concat(devDependencies); args.push('--save-dev'); } if (verbose) { args.push('--verbose'); } const child = spawn(command, args, { stdio: 'inherit' }); child.on('close', (code) => { if (code !== 0) { reject({ command: `${command} ${args.join(' ')}` }); return; } resolve(); }); }); } function run(root, appName, verbose, originalDirectory, useYarn) { console.log(`Installing ${chalk.cyan('@evershop/evershop')}`); checkIfOnline(useYarn) .then((isOnline) => ({ isOnline })) .then(({ isOnline }) => { const allDependencies = ['@evershop/evershop']; return install(root, useYarn, allDependencies, verbose, isOnline).then( async () => { await installDevDependencies( root, useYarn, [ '@parcel/watcher', '@types/config', '@types/express', '@types/node', '@types/pg', '@types/react', 'execa', 'typescript' ], verbose, isOnline ); await createConfigFile(root); await createSampleExtension(root); await createSampleTheme(root); await setUpEverShop(root); } ); }) .catch((reason) => { console.log(reason); console.log(); console.log('Aborting installation.'); if (reason.command) { console.log(` ${chalk.cyan(reason.command)} has failed.`); } else { console.log(chalk.red('Unexpected error. Please report it as a bug:')); console.log(reason); } console.log(); // On 'exit' we will delete these files from target directory. const knownGeneratedFiles = [ 'package.json', 'node_modules', 'package-lock.json' ]; const currentFiles = fs.readdirSync(path.join(root)); currentFiles.forEach((file) => { knownGeneratedFiles.forEach((fileToMatch) => { // This removes all knownGeneratedFiles. if (file === fileToMatch) { console.log(`Deleting generated file... ${chalk.cyan(file)}`); fs.removeSync(path.join(root, file)); } }); }); const remainingFiles = fs.readdirSync(path.join(root)); if (!remainingFiles.length) { // Delete target folder if empty console.log( `Deleting ${chalk.cyan(`${appName}/`)} from ${chalk.cyan( path.resolve(root, '..') )}` ); process.chdir(path.resolve(root, '..')); fs.removeSync(path.join(root)); } console.log('Done.'); process.exit(1); }); } function checkNpmVersion() { let hasMinNpm = true; let npmVersion = null; try { npmVersion = execSync('npm --version').toString().trim(); hasMinNpm = semver.gte(npmVersion, '7.0.0'); } catch (err) { // ignore } return { hasMinNpm, npmVersion }; } function checkAppName(appName) { const validationResult = validateProjectName(appName); if (appName === 'dist') { console.error( chalk.red( `Cannot create a project named ${chalk.green( `"${appName}"` )} because it is reserved for the distribution files.\n` + `Please choose a different project name.` ) ); process.exit(1); } if (!validationResult.validForNewPackages) { console.error( chalk.red( `Cannot create a project named ${chalk.green( `"${appName}"` )} because of npm naming restrictions:\n` ) ); [ ...(validationResult.errors || []), ...(validationResult.warnings || []) ].forEach((error) => { console.error(chalk.red(` * ${error}`)); }); console.error(chalk.red('\nPlease choose a different project name.')); process.exit(1); } // TODO: there should be a single place that holds the dependencies const dependencies = ['react', 'react-dom', 'react-scripts'].sort(); if (dependencies.includes(appName)) { console.error( chalk.red( `Cannot create a project named ${chalk.green( `"${appName}"` )} because a dependency with the same name exists.\n` + `Due to the way npm works, the following names are not allowed:\n\n` ) + chalk.cyan(dependencies.map((depName) => ` ${depName}`).join('\n')) + chalk.red('\n\nPlease choose a different project name.') ); process.exit(1); } } // If project only contains files generated by GH, it’s safe. // Also, if project contains remnant error logs from a previous // installation, lets remove them now. // We also special case IJ-based products .idea because it integrates with CRA: // https://github.com/facebook/create-evershop-app/pull/368#issuecomment-243446094 function isSafeToCreateProjectIn(root, name) { const validFiles = [ '.DS_Store', '.git', '.gitattributes', '.gitignore', '.gitlab-ci.yml', '.hg', '.hgcheck', '.hgignore', '.idea', '.npmignore', '.travis.yml', 'docs', 'LICENSE', 'README.md', 'mkdocs.yml', 'Thumbs.db' ]; // These files should be allowed to remain on a failed install, but then // silently removed during the next create. const errorLogFilePatterns = [ 'npm-debug.log', 'yarn-error.log', 'yarn-debug.log' ]; const isErrorLog = (file) => errorLogFilePatterns.some((pattern) => file.startsWith(pattern)); const conflicts = fs .readdirSync(root) .filter((file) => !validFiles.includes(file)) // IntelliJ IDEA creates module files before CRA is launched .filter((file) => !/\.iml$/.test(file)) // Don't treat log files from previous installation as conflicts .filter((file) => !isErrorLog(file)); if (conflicts.length > 0) { console.log( `The directory ${chalk.green(name)} contains files that could conflict:` ); console.log(); for (const file of conflicts) { try { const stats = fs.lstatSync(path.join(root, file)); if (stats.isDirectory()) { console.log(` ${chalk.blue(`${file}/`)}`); } else { console.log(` ${file}`); } } catch (e) { console.log(` ${file}`); } } console.log(); console.log( 'Either try using a new directory name, or remove the files listed above.' ); return false; } // Remove any log files from a previous installation. fs.readdirSync(root).forEach((file) => { if (isErrorLog(file)) { fs.removeSync(path.join(root, file)); } }); return true; } function getProxy() { if (process.env.https_proxy) { return process.env.https_proxy; } else { try { // Trying to read https-proxy from .npmrc const httpsProxy = execSync('npm config get https-proxy') .toString() .trim(); return httpsProxy !== 'null' ? httpsProxy : undefined; } catch (e) {} } } // See https://github.com/facebook/create-evershop-app/pull/3355 function checkThatNpmCanReadCwd() { const cwd = process.cwd(); let childOutput = null; try { // Note: intentionally using spawn over exec since // the problem doesn't reproduce otherwise. // `npm config list` is the only reliable way I could find // to reproduce the wrong path. Just printing process.cwd() // in a Node process was not enough. childOutput = spawn.sync('npm', ['config', 'list']).output.join(''); } catch (err) { // Something went wrong spawning node. // Not great, but it means we can't do this check. // We might fail later on, but let's continue. return true; } if (typeof childOutput !== 'string') { return true; } const lines = childOutput.split('\n'); // `npm config list` output includes the following line: // "; cwd = C:\path\to\current\dir" (unquoted) // I couldn't find an easier way to get it. const prefix = '; cwd = '; const line = lines.find((line) => line.startsWith(prefix)); if (typeof line !== 'string') { // Fail gracefully. They could remove it. return true; } const npmCWD = line.substring(prefix.length); if (npmCWD === cwd) { return true; } console.error( chalk.red( `Could not start an npm process in the right directory.\n\n` + `The current directory is: ${chalk.bold(cwd)}\n` + `However, a newly started npm process runs in: ${chalk.bold( npmCWD )}\n\n` + `This is probably caused by a misconfigured system terminal shell.` ) ); if (process.platform === 'win32') { console.error( `${chalk.red( `On Windows, this can usually be fixed by running:\n\n` )} ${chalk.cyan( 'reg' )} delete "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n` + ` ${chalk.cyan( 'reg' )} delete "HKLM\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n\n${chalk.red( `Try to run the above two lines in the terminal.\n` )}${chalk.red( `To learn more about this problem, read: https://blogs.msdn.microsoft.com/oldnewthing/20071121-00/?p=24433/` )}` ); } return false; } function checkIfOnline(useYarn) { if (!useYarn) { // Don't ping the Yarn registry. // We'll just assume the best case. return Promise.resolve(true); } return new Promise((resolve) => { dns.lookup('registry.yarnpkg.com', (err) => { let proxy; if (err != null && (proxy = getProxy())) { // If a proxy is defined, we likely can't resolve external hostnames. // Try to resolve the proxy name as an indication of a connection. dns.lookup(url.parse(proxy).hostname, (proxyErr) => { resolve(proxyErr == null); }); } else { resolve(err == null); } }); }); } async function setUpEverShop(projectDir) { // Use spawn to run 'npm run setup' command from the project directory await new Promise((resolve, reject) => { const child = spawn('npm', ['run', 'setup'], { cwd: projectDir, stdio: 'inherit' }); child.on('close', (code) => { if (code !== 0) { reject({ command: 'npm run setup' }); return; } resolve(); }); }); } async function createConfigFile(projectDir) { console.log( `Creating ${chalk.cyan('config/default.json')} in ${chalk.green( projectDir )}` ); const config = { shop: { language: 'en', currency: 'USD' }, system: { extensions: [ { name: 'sample', resolve: 'extensions/sample', enabled: true } ], theme: 'sample' } }; await mkdir(path.resolve(projectDir, 'config'), { recursive: true }); fs.writeFileSync( path.join(projectDir, 'config', 'default.json'), JSON.stringify(config, null, 2) + os.EOL ); } async function createSampleExtension(projectDir) { console.log( `Creating ${chalk.cyan('extensions/sample')} in ${chalk.green(projectDir)}` ); // Copy the extensions folder from the package to the project directory const sourceDir = path.resolve(__dirname, 'sample/extensions'); const targetDir = path.resolve(projectDir, 'extensions'); await fs.copy(sourceDir, targetDir); } async function createSampleTheme(projectDir) { console.log( `Creating ${chalk.cyan('themes/sample')} in ${chalk.green(projectDir)}` ); // Copy the themes folder from the package to the project directory const sourceDir = path.resolve(__dirname, 'sample/themes'); const targetDir = path.resolve(projectDir, 'themes'); await fs.copy(sourceDir, targetDir); } function checkForLatestVersion() { return new Promise((resolve, reject) => { https .get( 'https://registry.npmjs.org/-/package/create-evershop-app/dist-tags', (res) => { if (res.statusCode === 200) { let body = ''; res.on('data', (data) => (body += data)); res.on('end', () => { resolve(JSON.parse(body).latest); }); } else { reject(); } } ) .on('error', () => { reject(); }); }); } module.exports = { init }; ================================================ FILE: packages/create-evershop-app/index.js ================================================ #!/usr/bin/env node const currentNodeVersion = process.versions.node; const semver = currentNodeVersion.split('.'); const major = semver[0]; if (major < 14) { console.error( `You are running Node ${currentNodeVersion}.\n` + `Create React App requires Node 14 or higher. \n` + `Please update your version of Node.` ); process.exit(1); } const { init } = require('./createEverShopApp'); init(); ================================================ FILE: packages/create-evershop-app/package.json ================================================ { "name": "create-evershop-app", "version": "2.3.0", "description": "Create EverShop App", "main": "index.js", "files": [ "index.js", "createEverShopApp.js", "sample" ], "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "bin": { "create-evershop-app": "./index.js" }, "repository": { "type": "git", "url": "git+https://github.com/evershopcommerce/evershop.git" }, "author": "The Nguyen", "license": "ISC", "bugs": { "url": "https://github.com/evershopcommerce/evershop/issues" }, "homepage": "https://github.com/evershopcommerce/evershop#readme", "dependencies": { "boxen": "^5.1.2", "chalk": "^4.1.2", "commander": "^9.4.1", "cross-spawn": "^7.0.3", "fs-extra": "^10.0.0", "semver": "^7.6.3", "validate-npm-package-name": "^4.0.0" } } ================================================ FILE: packages/create-evershop-app/sample/themes/sample/dist/pages/all/EveryWhere.d.ts ================================================ import React from 'react'; export default function EveryWhere(): React.JSX.Element; export declare const layout: { areaId: string; sortOrder: number; }; ================================================ FILE: packages/create-evershop-app/sample/themes/sample/dist/pages/all/EveryWhere.js ================================================ "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.layout = void 0; exports.default = EveryWhere; const react_1 = __importDefault(require("react")); function EveryWhere() { return (react_1.default.createElement("div", { className: "container mx-auto px-4 py-8 bg-gray-100 rounded-lg shadow-md mt-10" }, react_1.default.createElement("h1", { className: "font-bold text-center mb-6" }, "Everywhere"), react_1.default.createElement("p", { className: "text-gray-700 text-center" }, "This component is rendered on every page of the store front."), react_1.default.createElement("p", { className: "text-gray-700 text-center" }, "You can modify this component at", ' ', react_1.default.createElement("code", null, "`themes/sample/src/pages/all/EveryWhere.tsx`")), react_1.default.createElement("p", { className: " text-gray-700 text-center" }, "You can also remove this by disabling the theme `sample`."))); } exports.layout = { areaId: 'content', sortOrder: 20 }; //# sourceMappingURL=EveryWhere.js.map ================================================ FILE: packages/create-evershop-app/sample/themes/sample/dist/pages/all/EveryWhere.js.map ================================================ {"version":3,"file":"EveryWhere.js","sourceRoot":"","sources":["../../../src/pages/all/EveryWhere.tsx"],"names":[],"mappings":";;;;;;AAEA,6BAgBC;AAlBD,kDAA0B;AAE1B,SAAwB,UAAU;IAChC,OAAO,CACL,uCAAK,SAAS,EAAC,oEAAoE;QACjF,sCAAI,SAAS,EAAC,4BAA4B,iBAAgB;QAC1D,qCAAG,SAAS,EAAC,2BAA2B,mEAEpC;QACJ,qCAAG,SAAS,EAAC,2BAA2B;;YACL,GAAG;YACpC,2FAAyD,CACvD;QACJ,qCAAG,SAAS,EAAC,4BAA4B,gEAErC,CACA,CACP,CAAC;AACJ,CAAC;AAEY,QAAA,MAAM,GAAG;IACpB,MAAM,EAAE,SAAS;IACjB,SAAS,EAAE,EAAE;CACd,CAAC"} ================================================ FILE: packages/create-evershop-app/sample/themes/sample/dist/pages/homepage/OnlyHomePage.d.ts ================================================ import React from 'react'; export default function OnlyHomePage(): React.JSX.Element; export declare const layout: { areaId: string; sortOrder: number; }; ================================================ FILE: packages/create-evershop-app/sample/themes/sample/dist/pages/homepage/OnlyHomePage.js ================================================ "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.layout = void 0; exports.default = OnlyHomePage; const react_1 = __importDefault(require("react")); function OnlyHomePage() { return (react_1.default.createElement("div", { className: "container mx-auto px-4 py-8 bg-gray-100 rounded-lg shadow-md mt-10" }, react_1.default.createElement("h1", { className: "font-bold text-center mb-6" }, "Home Page Only"), react_1.default.createElement("p", { className: " text-gray-700 text-center" }, "This component is only rendered on the home page."), react_1.default.createElement("p", { className: " text-gray-700 text-center" }, "You can modify this component at", ' ', react_1.default.createElement("code", null, "`themes/sample/src/pages/homepage/OnlyHomePage.tsx`")), react_1.default.createElement("p", { className: " text-gray-700 text-center" }, "You can also remove this by disabling the theme `sample`."))); } exports.layout = { areaId: 'content', sortOrder: 10 }; //# sourceMappingURL=OnlyHomePage.js.map ================================================ FILE: packages/create-evershop-app/sample/themes/sample/dist/pages/homepage/OnlyHomePage.js.map ================================================ {"version":3,"file":"OnlyHomePage.js","sourceRoot":"","sources":["../../../src/pages/homepage/OnlyHomePage.tsx"],"names":[],"mappings":";;;;;;AAEA,+BAgBC;AAlBD,kDAA0B;AAE1B,SAAwB,YAAY;IAClC,OAAO,CACL,uCAAK,SAAS,EAAC,oEAAoE;QACjF,sCAAI,SAAS,EAAC,4BAA4B,qBAAoB;QAC9D,qCAAG,SAAS,EAAC,4BAA4B,wDAErC;QACJ,qCAAG,SAAS,EAAC,4BAA4B;;YACN,GAAG;YACpC,kGAAgE,CAC9D;QACJ,qCAAG,SAAS,EAAC,4BAA4B,gEAErC,CACA,CACP,CAAC;AACJ,CAAC;AAEY,QAAA,MAAM,GAAG;IACpB,MAAM,EAAE,SAAS;IACjB,SAAS,EAAE,EAAE;CACd,CAAC"} ================================================ FILE: packages/create-evershop-app/sample/themes/sample/package.json ================================================ { "name": "sample-evershop-theme", "version": "1.0.0", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "tsc": "tsc" }, "keywords": [], "author": "", "license": "ISC", "description": "" } ================================================ FILE: packages/create-evershop-app/sample/themes/sample/src/pages/all/EveryWhere.tsx ================================================ import React from 'react'; export default function EveryWhere() { return (

Everywhere

This component is rendered on every page of the store front.

You can modify this component at{' '} `themes/sample/src/pages/all/EveryWhere.tsx`

You can also remove this by disabling the theme `sample`.

); } export const layout = { areaId: 'content', sortOrder: 20 }; ================================================ FILE: packages/create-evershop-app/sample/themes/sample/src/pages/homepage/OnlyHomePage.tsx ================================================ import React from 'react'; export default function OnlyHomePage() { return (

Home Page Only

This component is only rendered on the home page.

You can modify this component at{' '} `themes/sample/src/pages/homepage/OnlyHomePage.tsx`

You can also remove this by disabling the theme `sample`.

); } export const layout = { areaId: 'content', sortOrder: 10 }; ================================================ FILE: packages/create-evershop-app/sample/themes/sample/tsconfig.json ================================================ { "compilerOptions": { "module": "NodeNext", "target": "ES2018", "lib": ["dom", "dom.iterable", "esnext"], "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, "declaration": true, "sourceMap": true, "allowJs": true, "checkJs": false, "jsx": "react", "outDir": "./dist", "resolveJsonModule": true, "allowSyntheticDefaultImports": true, "allowArbitraryExtensions": true, "strictNullChecks": true, "isolatedModules": false, "baseUrl": ".", "rootDir": "./src" }, "include": ["src"] } ================================================ FILE: packages/evershop/.swcrc ================================================ { "$schema": "https://swc.rs/schema.json", "jsc": { "parser": { "syntax": "typescript", "tsx": true, "dynamicImport": true, "privateMethod": false, "functionBind": true, "exportDefaultFrom": true, "exportNamespaceFrom": false, "decorators": true, "decoratorsBeforeExport": false, "topLevelAwait": true, "importMeta": true, "importAs": true, "preserveAllComments": false }, "target": "es2022", "experimental": { "keepImportAssertions": true }, "loose": false, "keepClassNames": false }, "module": { "type": "es6" } } ================================================ FILE: packages/evershop/README.md ================================================

      

EverShop Logo

EverShop

Documentation | Demo

Github Action Twitter Follow Discord License

EverShop

## Introduction EverShop is a modern, TypeScript-first eCommerce platform built with GraphQL and React. Designed for developers, it offers essential commerce features in a modular, fully customizable architecture—perfect for building tailored shopping experiences with confidence and speed. ## Installation Using Docker You can get started with EverShop in minutes by using the Docker image. The Docker image is a great way to get started with EverShop without having to worry about installing dependencies or configuring your environment. ```bash curl -sSL https://raw.githubusercontent.com/evershopcommerce/evershop/main/docker-compose.yml > docker-compose.yml docker-compose up -d ``` For the full installation guide, please refer to our [Installation guide](https://evershop.io/docs/development/getting-started/installation-guide). ## Documentation - [Installation guide](https://evershop.io/docs/development/getting-started/installation-guide). - [Extension development](https://evershop.io/docs/development/module/create-your-first-extension). - [Theme development](https://evershop.io/docs/development/theme/theme-overview). ## Demo Explore our demo store.

evershop-backend-demo evershop-store-demo

Demo user: Email: demo@evershop.io
Password: 123456 ## Support If you like my work, feel free to: - ⭐ this repository. It helps. - [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)][tweet] about EverShop. Thank you! [tweet]: https://twitter.com/intent/tweet?url=https%3A%2F%2Fgithub.com%2Fevershopcommerce%2Fevershop&text=Awesome%20React%20Ecommerce%20Project&hashtags=react,ecommerce,expressjs,graphql ## Contributing EverShop is an open-source project. We are committed to a fully transparent development process and appreciate highly any contributions. Whether you are helping us fix bugs, proposing new features, improving our documentation or spreading the word - we would love to have you as part of the EverShop community. ### Ask a question about EverShop You can ask questions, and participate in discussions about EverShop-related topics in the EverShop Discord channel. ### Create a bug report If you see an error message or run into an issue, please [create bug report](https://github.com/evershopcommerce/evershop/issues/new). This effort is valued and it will help all EverShop users. ### Submit a feature request If you have an idea, or you're missing a capability that would make development easier and more robust, please [Submit feature request](https://github.com/evershopcommerce/evershop/issues/new). If a similar feature request already exists, don't forget to leave a "+1". If you add some more information such as your thoughts and vision about the feature, your comments will be embraced warmly :) Please refer to our [Contribution Guidelines](./CONTRIBUTING.md) and [Code of Conduct](./CODE_OF_CONDUCT.md). ## License [GPL-3.0 License](https://github.com/evershopcommerce/evershop/blob/main/LICENSE) ================================================ FILE: packages/evershop/package.json ================================================ { "name": "@evershop/evershop", "version": "2.1.1", "type": "module", "description": "The React Ecommerce platform. Built with Typescript, React and Postgres. Open-source and free. Fast and customizable.", "files": [ "dist", "src", ".swcrc" ], "bin": { "evershop": "./dist/bin/evershop.js" }, "exports": { "./types/*": { "types": "./dist/types/*.d.ts" }, "./lib/helpers": { "import": "./dist/lib/helpers.js", "types": "./dist/lib/helpers.d.ts" }, "./lib/mail/*": { "import": "./dist/lib/mail/*.js", "types": "./dist/lib/mail/*.d.ts" }, "./lib/util/*": { "import": "./dist/lib/util/*.js", "types": "./dist/lib/util/*.d.ts" }, "./lib/event": { "import": "./dist/lib/event/emitter.js", "types": "./dist/lib/event/emitter.d.ts" }, "./lib/event/subscriber": { "import": "./dist/lib/event/subscriber.js", "types": "./dist/lib/event/subscriber.d.ts" }, "./lib/postgres": { "import": "./dist/lib/postgres/connection.js", "types": "./dist/lib/postgres/connection.d.ts" }, "./lib/locale/*": { "import": "./dist/lib/locale/*.js", "types": "./dist/lib/locale/*.d.ts" }, "./lib/log": { "import": "./dist/lib/log/logger.js", "types": "./dist/lib/log/logger.d.ts" }, "./lib/router": { "import": "./dist/lib/router/index.js", "types": "./dist/lib/router/index.d.ts" }, "./lib/widget": { "import": "./dist/lib/widget/widgetManager.js", "types": "./dist/lib/widget/widgetManager.d.ts" }, "./lib/cronjob": { "import": "./dist/lib/cronjob/jobManager.js", "types": "./dist/lib/cronjob/jobManager.d.ts" }, "./lib/middleware/delegate": { "import": "./dist/lib/middleware/delegate.js", "types": "./dist/lib/middleware/delegate.d.ts" }, "./components/common/*": { "import": "./dist/components/common/*.js", "types": "./dist/components/common/*.d.ts" }, "./components/admin/*": { "import": "./dist/components/admin/*.js", "types": "./dist/components/admin/*.d.ts" }, "./components/frontStore/*": { "import": "./dist/components/frontStore/*.js", "types": "./dist/components/frontStore/*.d.ts" }, "./graphql/services": { "import": "./dist/modules/graphql/services/index.js", "types": "./dist/modules/graphql/services/index.d.ts" }, "./catalog/services": { "import": "./dist/modules/catalog/services/index.js", "types": "./dist/modules/catalog/services/index.d.ts" }, "./customer/services": { "import": "./dist/modules/customer/services/index.js", "types": "./dist/modules/customer/services/index.d.ts" }, "./setting/services": { "import": "./dist/modules/setting/services/index.js", "types": "./dist/modules/setting/services/index.d.ts" }, "./checkout/services": { "import": "./dist/modules/checkout/services/index.js", "types": "./dist/modules/checkout/services/index.d.ts" }, "./oms/services": { "import": "./dist/modules/oms/services/index.js", "types": "./dist/modules/oms/services/index.d.ts" }, "./cms/services": { "import": "./dist/modules/cms/services/index.js", "types": "./dist/modules/cms/services/index.d.ts" }, "./package.json": "./package.json" }, "scripts": { "prepack": "rimraf dist && tsc && copyfiles -u 1 \"src/**/*.{graphql,scss,css,json}\" dist", "dev": "node ./dist/bin/dev/index.js", "start": "node ./dist/bin/start/index.js", "build": "node ./dist/bin/build/index.js", "build-fast": "evershop build -- --skip-minify", "user:create": "evershop user:create", "user:changePassword": "evershop user:changePassword", "test": "jest" }, "author": "The Nguyen (https://evershop.io)", "license": "GNU GENERAL PUBLIC LICENSE 3.0", "repository": { "type": "git", "url": "git+https://github.com/evershopcommerce/evershop.git" }, "keywords": [ "ecommerce", "shopping cart", "cart" ], "bugs": { "url": "https://github.com/evershopcommerce/evershop/issues" }, "homepage": "http://evershop.io/", "dependencies": { "@base-ui/react": "^1.1.0", "@ckeditor/ckeditor5-build-classic": "^36.0.1", "@ckeditor/ckeditor5-react": "^5.1.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@editorjs/editorjs": "^2.30.8", "@editorjs/header": "^2.8.7", "@editorjs/list": "^1.10.0", "@editorjs/quote": "^2.6.0", "@editorjs/raw": "^2.5.0", "@evershop/editorjs-image": "^1.1.0", "@evershop/postgres-query-builder": "^2.0.1", "@graphql-tools/load-files": "^6.6.1", "@graphql-tools/merge": "^8.4.2", "@graphql-tools/schema": "^9.0.19", "@hapi/topo": "^5.0.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.6.0", "@stripe/react-stripe-js": "^1.5.0", "@stripe/stripe-js": "^1.18.0", "@swc/cli": "^0.7.7", "@swc/core": "^1.11.29", "@tailwindcss/postcss": "^4.1.18", "@tailwindcss/typography": "^0.5.13", "ajv": "^8.12.0", "ajv-errors": "^3.0.0", "ajv-formats": "^2.1.1", "autoprefixer": "^10.4.13", "axios": "^1.13.2", "bcryptjs": "^2.4.3", "body-parser": "^1.20.0", "boxen": "^5.1.2", "class-variance-authority": "^0.7.1", "clean-css": "^5.3.1", "clsx": "^2.1.1", "config": "^3.3.6", "connect-pg-simple": "^9.0.0", "cookie-parser": "^1.4.6", "cross-spawn": "^7.0.6", "css-loader": "^6.7.1", "csv-parser": "^3.0.0", "dayjs": "^1.10.6", "debug": "^4.3.2", "dotenv": "^16.3.1", "enquirer": "^2.3.6", "execa": "^9.6.0", "express": "^4.21.2", "express-session": "^1.17.3", "fast-glob": "^3.3.3", "flatpickr": "^4.6.9", "graphql": "^16.6.0", "graphql-tag": "^2.12.6", "graphql-type-json": "^0.3.2", "handlebars": "^4.7.8", "html-entities": "^2.3.3", "html-webpack-plugin": "^5.5.0", "immer": "^10.1.1", "jsesc": "^3.0.2", "json5": "^2.2.1", "jsonwebtoken": "^9.0.2", "kleur": "3.0.3", "lodash.isequalwith": "^4.4.0", "lucide-react": "^0.562.0", "luxon": "^2.0.2", "mini-css-extract-plugin": "^2.6.1", "minimatch": "^10.2.3", "multer": "^2.1.1", "node-cron": "^3.0.3", "ora": "^5.4.1", "pg": "^8.16.3", "postcss": "^8.4.18", "postcss-loader": "^8.2.0", "prop-types": "^15.8.1", "react": "^17.0.1", "react-dom": "^17.0.1", "react-fast-compare": "^3.2.0", "react-hook-form": "^7.61.1", "react-refresh": "^0.14.0", "react-select": "^5.4.0", "react-slick": "^0.31.0", "react-toastify": "^6.2.0", "recharts": "^2.0.9", "sanitize-html": "^2.17.0", "sass": "^1.53.0", "sass-loader": "^13.0.2", "semver": "^7.6.3", "serve-static": "^1.15.0", "session-file-store": "^1.5.0", "sharp": "^0.33.5", "slick-carousel": "^1.8.1", "stripe": "^8.176.0", "style-loader": "^3.3.1", "swc-minify-webpack-plugin": "^2.1.3", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", "touch": "^3.1.1", "tw-animate-css": "^1.4.0", "uniqid": "^5.3.0", "urql": "^3.0.3", "uuid": "^9.0.0", "webpack": "^5.72.1", "webpack-dev-middleware": "^7.4.2", "webpack-hot-middleware": "^2.26.1", "webpackbar": "^5.0.2", "winston": "^3.3.3", "yargs": "^17.7.2", "zero-decimal-currencies": "^1.2.0" }, "devDependencies": { "@parcel/watcher": "^2.5.1", "@paypal/paypal-js": "^8.4.2", "@types/config": "^3.3.5", "@types/express": "^5.0.1", "@types/express-session": "^1.18.2", "@types/multer": "^2.0.0", "@types/node": "^22.14.1", "@types/pg": "^8.15.2", "@types/react": "^19.1.2", "@types/sanitize-html": "^2.16.0", "copyfiles": "^2.4.1", "typescript": "^5.8.3" } } ================================================ FILE: packages/evershop/scripts/postpack.js ================================================ import fs from 'fs'; import path from 'path'; import packageJson from '../package.json' with { type: 'json' }; // Get the current version of the package from the nearest package.json file const { version } = packageJson; // Get the --pack-destination from the command line arguments // Create a package.json file in the packDestination directory with dependencies is the package itself fs.writeFileSync( path.resolve(process.env.npm_config_pack_destination, 'package.json'), JSON.stringify( { name: packageJson.name, version, dependencies: { '@evershop/evershop': `file:./evershop-evershop-${version}.tgz` }, scripts: { setup: 'evershop install', start: 'evershop start', 'start:debug': 'evershop start:debug', build: 'evershop build', dev: 'evershop dev', 'user:create': 'evershop user:create' } }, null, 2 ) ); ================================================ FILE: packages/evershop/scripts/postpublish.js ================================================ import fs from 'fs'; import path from 'path'; function getFileRecursive(dir, files) { const list = fs.readdirSync(dir); list.forEach((file) => { const filePath = path.join(dir, file); const stat = fs.statSync(filePath); if (stat.isDirectory()) { getFileRecursive(filePath, files); } else { files.push(filePath); } }); } const files = []; getFileRecursive(path.resolve(__dirname, './bin/serve'), files); files.forEach((file) => { const source = fs.readFileSync(file, { encoding: 'utf8', flag: 'r' }); const result = source.replace(/\.\.\/dist/g, '../src'); fs.writeFileSync(file, result, 'utf8'); }); ================================================ FILE: packages/evershop/scripts/prepublish.js ================================================ import fs from 'fs'; import path from 'path'; fs.copyFile( path.resolve(__dirname, '../../README.md'), path.resolve(__dirname, './README.md'), (err) => { if (err) throw err; } ); ================================================ FILE: packages/evershop/src/bin/build/client/index.js ================================================ import webpack from 'webpack'; import { error } from '../../../src/lib/log/logger'; import { createConfigClient } from '../../../src/lib/webpack/prod/createConfigClient'; export async function buildClient(routes) { const config = createConfigClient(routes); const compiler = webpack(config); return new Promise((resolve, reject) => { compiler.run((err, stats) => { if (err || stats.hasErrors()) { error( stats.toString({ errorDetails: true, warnings: true }) ); reject(err); } resolve(stats); }); }); } ================================================ FILE: packages/evershop/src/bin/build/complie.js ================================================ import pkg from 'webpack'; import { error } from '../../lib/log/logger.js'; import { createConfigClient } from '../../lib/webpack/prod/createConfigClient.js'; import { createConfigServer } from '../../lib/webpack/prod/createConfigServer.js'; const { webpack } = pkg; export async function compile(routes) { const config = [createConfigClient(routes), createConfigServer(routes)]; const compiler = webpack(config); return new Promise((resolve, reject) => { compiler.run((err, stats) => { if (err || stats.hasErrors()) { if (err) { error(err); } error( stats.toString({ errorDetails: true, warnings: true }) ); reject(err); } resolve(stats); }); }); } ================================================ FILE: packages/evershop/src/bin/build/index.js ================================================ import { existsSync, mkdirSync, rmSync } from 'fs'; import path from 'path'; import config from 'config'; import { CONSTANTS } from '../../lib/helpers.js'; import { error } from '../../lib/log/logger.js'; import { loadModuleRoutes } from '../../lib/router/loadModuleRoutes.js'; import { getRoutes } from '../../lib/router/Router.js'; import { lockHooks } from '../../lib/util/hookable.js'; import { lockRegistry } from '../../lib/util/registry.js'; import { validateConfiguration } from '../../lib/util/validateConfiguration.js'; import { isBuildRequired } from '../../lib/webpack/isBuildRequired.js'; import { getEnabledExtensions } from '../extension/index.js'; import { loadBootstrapScript } from '../lib/bootstrap/bootstrap.js'; import { buildEntry } from '../lib/buildEntry.js'; import { getCoreModules } from '../lib/loadModules.js'; import { compile } from './complie.js'; import './initEnvBuild.js'; /* Loading modules and initilize routes, components */ const modules = [...getCoreModules(), ...getEnabledExtensions()]; /** Loading routes */ modules.forEach((module) => { try { // Load routes loadModuleRoutes(module.path); } catch (e) { error(e); process.exit(0); } }); /** Clean up the build directory */ if (existsSync(path.resolve(CONSTANTS.BUILDPATH))) { // Delete directory recursively rmSync(path.resolve(CONSTANTS.BUILDPATH), { recursive: true }); mkdirSync(path.resolve(CONSTANTS.BUILDPATH)); } else { mkdirSync(path.resolve(CONSTANTS.BUILDPATH), { recursive: true }); } export default async function build() { /** Loading bootstrap script from modules */ try { for (const module of modules) { await loadBootstrapScript(module, { command: 'build', env: 'production', process: 'main' }); } lockHooks(); lockRegistry(); // Get the configuration (nodeconfig) validateConfiguration(config); } catch (e) { error(e); process.exit(1); } process.env.ALLOW_CONFIG_MUTATIONS = false; const routes = getRoutes(); await buildEntry(routes.filter((r) => isBuildRequired(r))); /** Build */ await compile(routes); } process.on('uncaughtException', function (exception) { import('../../lib/log/logger.js').then((module) => { module.error(exception); }); }); process.on('unhandledRejection', (reason, p) => { import('../../lib/log/logger.js').then((module) => { module.error(`Unhandled Rejection: ${reason} at: ${p}`); }); }); build(); ================================================ FILE: packages/evershop/src/bin/build/initEnvBuild.ts ================================================ import 'dotenv/config'; process.env.NODE_ENV = 'production'; process.env.ALLOW_CONFIG_MUTATIONS = 'true'; ================================================ FILE: packages/evershop/src/bin/build/server/index.js ================================================ import pkg from 'webpack'; import { error } from '../../../src/lib/log/logger.js'; import { createConfigServer } from '../../../src/lib/webpack/prod/createConfigServer.js'; const { webpack } = pkg; export const buildServer = async function buildServer(routes) { const config = createConfigServer(routes); const compiler = webpack(config); return new Promise((resolve, reject) => { compiler.run((err, stats) => { if (err || stats.hasErrors()) { error( stats.toString({ errorDetails: true, warnings: true }) ); reject(err); } resolve(stats); }); }); }; ================================================ FILE: packages/evershop/src/bin/build/server/useDDL.js ================================================ import { existsSync, rmdirSync } from 'fs'; import { mkdir, writeFile } from 'fs/promises'; import path from 'path'; import { inspect } from 'util'; import boxen from 'boxen'; import { green, red } from 'kleur'; import ora from 'ora'; import pkg from 'webpack'; import { getComponentsByRoute } from '../../../src/lib/componee/getComponentByRoute.js'; import { CONSTANTS } from '../../../src/lib/helpers.js'; import { info } from '../../../src/lib/log/logger.js'; import { getRoutes } from '../../../src/lib/router/routes.js'; // Run building vendor first import { createVendorConfig } from '../../../src/lib/webpack/configProvider.js'; import { loadModuleComponents } from '../../serve/loadModuleComponents.js'; import { loadModuleRoutes } from '../../serve/loadModuleRoutes.js'; import { loadModules } from '../../serve/loadModules.js'; const { webpack } = pkg; const modules = loadModules(path.resolve(__dirname, '../../../src', 'modules')); const spinner = ora({ text: green('Starting server build'), spinner: 'dots12' }).start(); spinner.start(); // Initilizing routes modules.forEach((module) => { try { // Load routes loadModuleRoutes(module.path); } catch (e) { spinner.fail(`${red(e.stack)}\n`); process.exit(0); } }); // Initializing components modules.forEach((module) => { try { // Load components loadModuleComponents(module.path); } catch (e) { spinner.fail(`${red(e.stack)}\n`); process.exit(0); } }); const routes = getRoutes(); // Collect all "controller" route const controllers = routes.filter((r) => r.isApi === false); const promises = []; const total = controllers.length - 1; let completed = 0; spinner.text = `Start building ☕☕☕☕☕\n${Array(total).fill('▒').join('')}`; if (existsSync(path.resolve(CONSTANTS.ROOTPATH, './.evershop/build'))) { rmdirSync(path.resolve(CONSTANTS.ROOTPATH, './.evershop/build'), { recursive: true }); } const start = Date.now(); const vendorComplier = webpack(createVendorConfig(webpack)); const webpackVendorPromise = new Promise((resolve, reject) => { vendorComplier.run((err, stats) => { if (err) { reject(err); } else if (stats.hasErrors()) { reject( new Error( stats.toString({ errorDetails: true, warnings: true }) ) ); } else { resolve(stats); } }); }); webpackVendorPromise.then(async () => { controllers.forEach((route) => { const buildFunc = async function () { const components = getComponentsByRoute(route.id); if (!components) { return; } Object.keys(components).forEach((area) => { Object.keys(components[area]).forEach((id) => { components[area][ id ].component = `---require("${components[area][id].source}")---`; delete components[area][id].source; }); }); const buildPath = route.isAdmin === true ? `./admin/${route.id}` : `./frontStore/${route.id}`; let content = `var components = module.exports = exports = ${inspect( components, { depth: 5 } ) .replace(/'---/g, '') .replace(/---'/g, '')}`; content += '\r\n'; await mkdir( path.resolve(CONSTANTS.ROOTPATH, './.evershop/build', buildPath), { recursive: true } ); await writeFile( path.resolve( CONSTANTS.ROOTPATH, '.evershop/build', buildPath, 'components.js' ), content ); const name = route.isAdmin === true ? `admin/${route.id}` : `frontStore/${route.id}`; const entry = {}; entry[name] = [ path.resolve( CONSTANTS.ROOTPATH, '.evershop', 'build', buildPath, 'components.js' ), path.resolve( CONSTANTS.LIBPATH, '../components/common/react/server', 'render.js' ) ]; const compiler = webpack({ mode: 'production', // "production" | "development" | "none" module: { rules: [ { test: /\/views|components|context\/(.*).js?$/, // test: /\.js?$/, exclude: /(bower_components)/, use: { loader: 'babel-loader?cacheDirectory', options: { sourceType: 'unambiguous', cacheDirectory: true, presets: [ [ '@babel/preset-env', { exclude: [ '@babel/plugin-transform-regenerator', '@babel/plugin-transform-async-to-generator' ] } ], '@babel/preset-react' ] } } }, { test: /getComponents\.js/, use: [ { loader: path.resolve( CONSTANTS.LIBPATH, 'webpack/getComponentLoader.js' ), options: { componentsPath: path.resolve( CONSTANTS.ROOTPATH, './.evershop/build', buildPath, 'components.js' ) } } ] } ] }, // name: 'main', target: 'node12.18', entry, output: { path: path.resolve( CONSTANTS.ROOTPATH, './.evershop/build', buildPath, 'server' ), libraryTarget: 'commonjs2', globalObject: 'this', filename: 'index.js' }, resolve: { alias: { react: path.resolve(CONSTANTS.NODEMODULEPATH, 'react') } }, plugins: [ new webpack.DllReferencePlugin({ manifest: path.resolve( CONSTANTS.ROOTPATH, './.evershop/build/vendor-manifest.json' ) }) ] }); const webpackPromise = new Promise((resolve, reject) => { compiler.run((err, stats) => { if (err) { reject(err); } else if (stats.hasErrors()) { reject( new Error( stats.toString({ errorDetails: true, warnings: true }) ) ); } else { resolve(stats); } }); }); await webpackPromise; completed += 1; spinner.text = `Start building ☕☕☕☕☕\n${Array(completed) .fill(green('█')) .concat(total - completed > 0 ? Array(total - completed).fill('▒') : []) .join('')}`; }; promises.push(buildFunc()); }); await Promise.all(promises) .then(() => { spinner.succeed( green('Building completed!!!\n') + boxen(green('Please run "npm run start" to start your website'), { title: 'EverShop', titleAlignment: 'center', padding: 1, margin: 1, borderColor: 'green' }) ); const end = Date.now(); info(`Execution time: ${end - start} ms`); process.exit(0); }) .catch((e) => { spinner.fail(`${red(e)}\n`); process.exit(0); }); }); ================================================ FILE: packages/evershop/src/bin/build/server/useVendorChunk.js ================================================ import { existsSync, rmSync } from 'fs'; import path from 'path'; import { green, red } from 'kleur'; import ora from 'ora'; import webpack from 'webpack'; import { CONSTANTS } from '../../../src/lib/helpers.js'; import { getRoutes } from '../../../src/lib/router/routes.js'; import { createConfig } from '../../../src/lib/webpack/createConfig.js'; import { loadModuleComponents } from '../../serve/loadModuleComponents.js'; import { loadModuleRoutes } from '../../serve/loadModuleRoutes.js'; import { loadModules } from '../../serve/loadModules.js'; import { createComponents } from '../createComponents.js'; (async () => { const start = Date.now(); const modules = loadModules( path.resolve(__dirname, '../../../src', 'modules') ); const spinner = ora({ text: green('Starting server build'), spinner: 'dots12' }).start(); spinner.start(); /** Initilizing routes */ modules.forEach((module) => { try { // Load routes loadModuleRoutes(module.path); } catch (e) { spinner.fail(`${red(e.stack)}\n`); process.exit(0); } }); /** Initializing components */ modules.forEach((module) => { try { // Load components loadModuleComponents(module.path); } catch (e) { spinner.fail(`${red(e.stack)}\n`); process.exit(0); } }); /** Get list of routes */ const routes = getRoutes(); /** Collect all 'controller' routes */ const controllers = routes.filter((r) => r.isApi === false); /** Clean up the build directory */ if (existsSync(CONSTANTS.BUILDPATH)) { rmSync(CONSTANTS.BUILDPATH, { recursive: true }); } /** Create components.js file for each route */ await createComponents(controllers); /** Create the webpack complier object */ const compiler = webpack(createConfig(true, controllers)); /** Run the build */ await new Promise((resolve, reject) => { compiler.run((err, stats) => { if (err) { reject(err); } else if (stats.hasErrors()) { reject( new Error( stats.toString({ errorDetails: true, warnings: true }) ) ); } else { resolve(stats); } }); }); const end = Date.now(); spinner.succeed(`${green('Server build completed in')} ${end - start}ms`); process.exit(0); })(); ================================================ FILE: packages/evershop/src/bin/dev/compileTs.js ================================================ import path from 'path'; import { compileSwc } from '../lib/watch/compileSwc.js'; import { getSrcPaths } from '../lib/watch/getSrcPaths.js'; async function compileTs() { const srcPaths = getSrcPaths(); const events = srcPaths.map((srcPath) => { return { srcPath: srcPath, distPath: path.resolve(srcPath, '..', 'dist') }; }); await Promise.all( events.map((event) => { return compileSwc(event.srcPath, event.distPath); }) ); } export { compileTs }; ================================================ FILE: packages/evershop/src/bin/dev/enableWatcher.js ================================================ import { subscribe } from '@parcel/watcher'; import { CONSTANTS } from '../../lib/helpers.js'; import { watchHandler } from '../lib/watch/watchHandler.js'; export default async function enableWatcher() { const watcherInstance = await subscribe( CONSTANTS.ROOTPATH, (err, events) => { if (err) { return; } watchHandler(events); }, { ignore: [ '**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**', '**/.cache/**', '**/.next/**', '**/.nuxt/**', '**/.vscode/**' ] } ); process.on('SIGINT', () => { watcherInstance.unsubscribe(); process.exit(0); }); process.on('SIGTERM', () => { watcherInstance.unsubscribe(); }); process.on('exit', () => { watcherInstance.unsubscribe(); }); } ================================================ FILE: packages/evershop/src/bin/dev/hooks.js ================================================ import { isBuiltin } from 'node:module'; import path, { dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; let broadcastChannel; export function initialize(data) { broadcastChannel = data.broadcastChannel; } export function resolve(specifier, context, nextResolve) { if ( isBuiltin(specifier) || specifier.includes('?t=') || context.parentURL === undefined ) { return nextResolve(specifier, context); } else { const modulePath = !specifier.startsWith('file:') ? path.resolve(dirname(fileURLToPath(context.parentURL)), specifier) : fileURLToPath(specifier); if (modulePath.includes('node_modules')) { return nextResolve(specifier, context); } else { broadcastChannel.postMessage({ path: modulePath, }); return nextResolve(specifier, context); } } } ================================================ FILE: packages/evershop/src/bin/dev/index.ts ================================================ import path from 'path'; import { fileURLToPath } from 'url'; import spawn from 'cross-spawn'; import { debug, error } from '../../lib/log/logger.js'; function startDev() { const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const args = [path.resolve(__dirname, 'init.js')]; const appProcess = spawn('node', args, { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], env: { ...process.env, ALLOW_CONFIG_MUTATIONS: true } }); appProcess.on('error', (err) => { error(`Error spawning processor: ${err}`); }); appProcess.on('message', (message) => { debug('Restarting the development server'); if (message === 'RESTART_ME') { if (appProcess && appProcess.pid) { appProcess.removeAllListeners(); appProcess.kill('SIGTERM'); } startDev(); } }); return appProcess; } const childProcess = startDev(); process.on('exit', () => { // Cleanup child processes on exit if (childProcess && childProcess.pid) { childProcess.kill(); } }); ================================================ FILE: packages/evershop/src/bin/dev/init.ts ================================================ import './register.js'; import './initEnvDev.js'; import { debug, error } from '../../lib/log/logger.js'; import { start } from '../lib/startUp.js'; import { compileTs } from './compileTs.js'; import enableWatcher from './enableWatcher.js'; await compileTs(); enableWatcher(); start({ command: 'dev', env: 'development', process: 'main' }); process.on('SIGTERM', async () => { debug('Received SIGTERM, shutting down the main process...'); try { process.exit(0); } catch (err) { error('Error during shutdown the main process:'); error(err); process.exit(1); } }); process.on('uncaughtException', function (exception) { import('../../lib/log/logger.js').then((module) => { module.error(exception); }); }); process.on('unhandledRejection', (reason, p) => { import('../../lib/log/logger.js').then((module) => { module.error(`Unhandled Rejection: ${reason} at: ${p}`); }); }); ================================================ FILE: packages/evershop/src/bin/dev/initEnvDev.ts ================================================ import 'dotenv/config'; process.env.NODE_ENV = 'development'; process.env.ALLOW_CONFIG_MUTATIONS = 'true'; ================================================ FILE: packages/evershop/src/bin/dev/register.js ================================================ import { register } from 'node:module'; import { MessageChannel } from 'node:worker_threads'; export const maps = new Map(); const { port1: listenChannel, port2: broadcastChannel } = new MessageChannel(); listenChannel.on('message', (message) => { maps.set(message.path, true); }); register('./hooks.js', { parentURL: import.meta.url, data: { broadcastChannel }, transferList: [broadcastChannel], }); export function has(pathName) { return maps.has(pathName); } ================================================ FILE: packages/evershop/src/bin/evershop.js ================================================ #!/usr/bin/env node import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; const { argv } = yargs(hideBin(process.argv)); const command = argv._[0]; try { if (command === 'build') { await import('./build/index.js'); } else if (command === 'dev') { await import('./dev/index.js'); } else if (command === 'start') { await import('./start/index.js'); } else if (command === 'install') { await import('./install/index.js'); } else if (command === 'user:create') { await import('./user/create.js'); } else if (command === 'user:changePassword') { await import('./user/changePassword.js'); } else if (command === 'theme:active') { await import('./theme/active.js'); } else if (command === 'theme:twizz') { await import('./theme/twizz.js'); } else if (command === 'theme:create') { await import('./theme/create.js'); } else if (command === 'seed') { await import('./seed/index.js'); } else { throw new Error('Invalid command'); } } catch (e) { import('../lib/log/logger.js').then((module) => { module.error(e); }); } process.on('uncaughtException', function (exception) { import('../lib/log/logger.js').then((module) => { module.error(exception); }); }); process.on('unhandledRejection', (reason, p) => { import('../lib/log/logger.js').then((module) => { module.error(`Unhandled Rejection: ${reason} at: ${p}`); }); }); ================================================ FILE: packages/evershop/src/bin/extension/index.ts ================================================ import { existsSync } from 'fs'; import { resolve } from 'path'; import { CONSTANTS } from '../../lib/helpers.js'; import { error, warning } from '../../lib/log/logger.js'; import { getConfig } from '../../lib/util/getConfig.js'; import { isDevelopmentMode } from '../../lib/util/isDevelopmentMode.js'; import { isProductionMode } from '../../lib/util/isProductionMode.js'; import { Extension } from '../../types/extension.js'; import { getCoreModules } from '../lib/loadModules.js'; let extensions: Extension[] | undefined = undefined; function loadExtensions(): Extension[] { const coreModules = getCoreModules(); const list = getConfig('system.extensions', []) as Extension[]; const extensions: Extension[] = []; list.forEach((extension) => { if ( coreModules.find((module) => module.name === extension.name) || extensions.find((e) => e.name === extension.name) ) { throw new Error( `Extension ${extension.name} is invalid. extension name must be unique.` ); } if (extension.enabled !== true) { warning(`Extension ${extension.name} is not enabled. Skipping.`); return; } if (!existsSync(extension.resolve)) { warning( `Extension ${extension.name} has resolve path ${extension.resolve} which does not exist. Skipping.` ); return; } if (isProductionMode() || extension.resolve.includes('node_modules')) { // Make sure the folder has 'dist' subdirectory if (!existsSync(resolve(extension.resolve, 'dist'))) { error( `Extension '${ extension.name }' must have a 'dist' directory at ${resolve( extension.resolve, 'dist' )}. This is required for production mode.` ); process.exit(1); } else { extensions.push({ ...extension, path: resolve(CONSTANTS.ROOTPATH, extension.resolve, 'dist') }); } } if (isDevelopmentMode() && !extension.resolve.includes('node_modules')) { // Make sure the folder has 'src' subdirectory if (!existsSync(resolve(extension.resolve, 'src'))) { error( `Extension '${ extension.name }' must have a 'src' directory at ${resolve( extension.resolve, 'src' )}` ); process.exit(1); } else { extensions.push({ ...extension, srcPath: resolve(extension.resolve, 'src'), path: resolve(extension.resolve, 'dist') }); } } }); // Sort the extensions by priority, smaller number means higher priority extensions.sort((a, b) => a.priority - b.priority); return extensions; } export function getEnabledExtensions() { if (extensions === undefined) { extensions = loadExtensions(); } return extensions; } ================================================ FILE: packages/evershop/src/bin/install/createMigrationTable.js ================================================ import { execute } from '@evershop/postgres-query-builder'; export async function createMigrationTable(connection) { await execute( connection, `CREATE TABLE IF NOT EXISTS "migration" ( "migration_id" INT GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1) PRIMARY KEY, "module" varchar NOT NULL, "version" varchar NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "MODULE_UNIQUE" UNIQUE ("module") )` ); } ================================================ FILE: packages/evershop/src/bin/install/index.js ================================================ import { mkdir, writeFile } from 'fs/promises'; import path from 'path'; import { commit, execute, insertOnUpdate, rollback, startTransaction } from '@evershop/postgres-query-builder'; import boxen from 'boxen'; import enquirer from 'enquirer'; import kleur from 'kleur'; import ora from 'ora'; import { Pool } from 'pg'; import { CONSTANTS } from '../../lib/helpers.js'; import { error, success } from '../../lib/log/logger.js'; import { hashPassword } from '../../lib/util/passwordHelper.js'; import { migrate } from '../lib/bootstrap/migrate.js'; import { getCoreModules } from '../lib/loadModules.js'; // The installation command will create a .env file in the root directory of the project. // If you are using docker, do not run this command. Instead, you should set the environment variables in the docker-compose.yml file and run `npm run start` // This command means for the developer who want to install the system on their local machine. async function install() { // Check if the env for database is set if (process.env.DB_HOST) { error( 'We found that you have already set the environment variables for the database. Look like you have already installed the system. Run `npm run build` and `npm run start` to launch your store.' ); process.exit(0); } var db; var adminUser; // eslint-disable-next-line no-console console.log( kleur.green( boxen('Welcome to EverShop - The open-source e-commerce platform', { title: 'EverShop', titleAlignment: 'center', padding: 1, margin: 1, borderColor: 'green' }) ) ); const dbQuestions = [ { type: 'input', name: 'databaseHost', message: 'Postgres Database Host (localhost)', initial: process.env.DB_HOST || 'localhost', skip: !!process.env.DB_HOST }, { type: 'input', name: 'databasePort', message: 'Postgres Database Port (5432)', initial: process.env.DB_PORT || 5432, skip: !!process.env.DB_PORT }, { type: 'input', name: 'databaseName', message: 'Postgres Database Name (evershop)', initial: process.env.DB_NAME || 'evershop', skip: !!process.env.DB_NAME }, { type: 'input', name: 'databaseUser', message: 'Postgres Database User (postgres)', initial: process.env.DB_USER || 'postgres', skip: !!process.env.DB_USER }, { type: 'input', name: 'databasePassword', message: 'PostgreSQL Database Password ()', initial: process.env.DB_PASSWORD || '', skip: !!process.env.DB_PASSWORD } ]; try { db = await enquirer.prompt(dbQuestions); } catch (e) { process.exit(0); } const baseDBSetting = { host: db.databaseHost, port: db.databasePort, user: db.databaseUser, password: db.databasePassword, database: db.databaseName, max: 10, idleTimeoutMillis: 30000 }; // We will try with SSL option enabled first let pool = new Pool({ ...baseDBSetting, ssl: true }); let sslMode; // Test the secure connection try { await pool.query(`SELECT 1`); sslMode = 'require'; } catch (e) { if (e.message.includes('does not support SSL')) { // If the database does not support SSL, we will try to connect without SSL pool = new Pool({ ...baseDBSetting, ssl: false }); sslMode = 'disable'; } else if (e.message.includes('certificate')) { error( `Looks like your database server does not have a valid SSL certificate. Please turn off the SSL option in the database configuration, restart the database server and try again.` ); } else { error(e); process.exit(0); } } // Check postgres database version try { const { rows } = await execute(pool, `SHOW SERVER_VERSION;`); if (rows[0].server_version < '13.0') { error( `Your database server version(${rows[0].server_version}) is not supported. Please upgrade to PostgreSQL version 13.0 or higher` ); process.exit(0); } } catch (e) { error(e); process.exit(0); } const adminUserQuestions = [ { type: 'input', name: 'fullName', message: 'Your full name', initial: process.env.ADMIN_FULLNAME || '', skip: !!process.env.ADMIN_FULLNAME }, { type: 'input', name: 'email', message: 'Your administrator user email', initial: process.env.ADMIN_EMAIL || 'admin@admin.com', skip: !!process.env.ADMIN_EMAIL, validate: (value) => { if ( !value.match( /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ ) ) { return 'Invalid email'; } return true; } }, { type: 'password', name: 'password', message: 'Your administrator user password', initial: process.env.ADMIN_PASSWORD || '123456', skip: !!process.env.ADMIN_PASSWORD, validate: (value) => { if (value.length < 8) { return 'Your password must be at least 8 characters.'; } if (value.search(/[a-z]/i) < 0) { return 'Your password must contain at least one letter.'; } if (value.search(/[0-9]/) < 0) { return 'Your password must contain at least one digit.'; } return true; } } ]; try { adminUser = await enquirer.prompt(adminUserQuestions); } catch (e) { process.exit(0); } /* Start installation */ const messages = []; messages.push(`\n\n${kleur.green('EverShop is being installed ☕ ☕ ☕')}`); messages.push('Creating .env file'); const spinner = ora({ text: kleur.green(messages.join('\n')), spinner: 'dots12' }).start(); spinner.start(); /** Create the .env file at the root folder with the database connection */ await writeFile( path.resolve(CONSTANTS.ROOTPATH, '.env'), `DB_HOST="${db.databaseHost}" DB_PORT="${db.databasePort}" DB_NAME="${db.databaseName}" DB_USER="${db.databaseUser}" DB_PASSWORD="${db.databasePassword}" DB_SSLMODE="${sslMode}" ` ); messages.pop(); messages.push(kleur.green('✔ Created .env file')); spinner.text = messages.join('\n'); // Create `media` folder await mkdir(path.resolve(CONSTANTS.ROOTPATH, 'media'), { recursive: true }); // Create `public` folder await mkdir(path.resolve(CONSTANTS.ROOTPATH, 'public'), { recursive: true }); // Start install database messages.push(kleur.green('Setting up a database')); spinner.text = messages.join('\n'); const connection = await pool.connect(); await startTransaction(connection); try { // Create the admin user const passwordHash = hashPassword(adminUser.password || '123456'); await execute( connection, `CREATE TABLE IF NOT EXISTS "admin_user" ( "admin_user_id" INT GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1) PRIMARY KEY, "uuid" UUID NOT NULL DEFAULT gen_random_uuid (), "status" boolean NOT NULL DEFAULT TRUE, "email" varchar NOT NULL, "password" varchar NOT NULL, "full_name" varchar DEFAULT NULL, "created_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "ADMIN_USER_EMAIL_UNIQUE" UNIQUE ("email"), CONSTRAINT "ADMIN_USER_UUID_UNIQUE" UNIQUE ("uuid") );` ); await insertOnUpdate('admin_user', ['email']) .given({ status: 1, email: adminUser?.email || 'admin@evershop.io', password: passwordHash, full_name: adminUser?.fullName || 'Admin' }) .execute(connection); // Run module migrations const coreModules = getCoreModules(); await migrate(coreModules, connection); await commit(connection); } catch (e) { await rollback(connection); error(e); process.exit(0); } messages.pop(); messages.push(kleur.green('✔ Setup database')); messages.push(kleur.green('✔ Create admin user')); spinner.succeed(messages.join('\n')); // eslint-disable-next-line no-console console.log( boxen( kleur.green( 'Installation completed!. Run `npm run build` and `npm run start` to launch your store' ), { title: 'EverShop', titleAlignment: 'center', padding: 1, margin: 1, borderColor: 'green' } ) ); process.exit(0); } (async () => { try { await install(); } catch (e) { error(e); process.exit(0); } })(); ================================================ FILE: packages/evershop/src/bin/install/templates/config.json ================================================ { "shop": { "currency": "USD", "language": "en", "weightUnit": "kg", "timezone": "UTC" }, "system": { "database": { "host": "localhost", "port": 5432, "database": "evershop", "user": "admin", "password": "123456" } } } ================================================ FILE: packages/evershop/src/bin/lib/addDefaultMiddlewareFuncs.ts ================================================ import { select } from '@evershop/postgres-query-builder'; import sessionStorage from 'connect-pg-simple'; import cookieParser from 'cookie-parser'; import session from 'express-session'; import pathToRegexp from 'path-to-regexp'; import { translate } from '../../lib/locale/translate/translate.js'; import { debug, warning } from '../../lib/log/logger.js'; import publicStatic from '../../lib/middlewares/publicStatic.js'; import themePublicStatic from '../../lib/middlewares/themePublicStatic.js'; import { pool } from '../../lib/postgres/connection.js'; import { getRoutes } from '../../lib/router/Router.js'; import { getConfig } from '../../lib/util/getConfig.js'; import isDevelopmentMode from '../../lib/util/isDevelopmentMode.js'; import isProductionMode from '../../lib/util/isProductionMode.js'; import { getAdminSessionCookieName } from '../../modules/auth/services/getAdminSessionCookieName.js'; import { getCookieSecret } from '../../modules/auth/services/getCookieSecret.js'; import { getFrontStoreSessionCookieName } from '../../modules/auth/services/getFrontStoreSessionCookieName.js'; import { setPageMetaInfo } from '../../modules/cms/services/pageMetaInfo.js'; import { getDevMiddleware, getHotMiddleware } from './devEnvHelper.js'; export function addDefaultMiddlewareFuncs(app) { app.use((request, response, next) => { response.debugMiddlewares = []; next(); response.on('finish', () => { // Console log the debug middlewares let message = `[${request.method}] ${request.originalUrl}\n`; response.debugMiddlewares.forEach((m) => { message += m.time ? `-> Middleware ${m.id} - ${m.time} ms\n` : `-> Middleware ${m.id}\n`; }); // Skip logging if the request is for static files if ( request.currentRoute?.id === 'staticAsset' || request.currentRoute?.id === 'adminStaticAsset' ) { return; } debug(message); }); }); // Add public static middleware app.use(publicStatic); // Add theme public static middleware app.use(themePublicStatic); // Express session const cookieSecret = getCookieSecret(); const sess = { store: process.env.NODE_ENV === 'test' ? undefined : new (sessionStorage(session))({ pool }), secret: cookieSecret, cookie: { maxAge: getConfig('system.session.maxAge', 24 * 60 * 60 * 1000) }, resave: getConfig('system.session.resave', false), saveUninitialized: getConfig('system.session.saveUninitialized', true) } as session.SessionOptions; if (isProductionMode()) { app.set('trust proxy', 1); sess.cookie!.secure = false; } const adminSessionMiddleware = session({ ...sess, name: getAdminSessionCookieName() }); const frontStoreSessionMiddleware = session({ ...sess, name: getFrontStoreSessionCookieName() }); // Cookie parser app.use(cookieParser(cookieSecret)); app.use((request, response, next) => { const routes = getRoutes(); const method = request.method.toUpperCase(); const requestPath = request.originalUrl.split('?')[0]; const matchedRoutes = routes.filter((r) => { const regexp = pathToRegexp(r.path, []); const match = regexp.exec(requestPath); if (match && r.method.includes(method)) { return true; } else { return false; } }); if (matchedRoutes.length > 1) { warning( `Multiple routes matched for ${requestPath}. Please check your routes: ${matchedRoutes .map((r) => r.id) .join(', ')}. Route ${matchedRoutes[0].id} will be used.` ); } if (matchedRoutes.length) { request.currentRoute = matchedRoutes[0]; next(); } else { next(); } }); const sessionMiddleware = (request, response, next) => { const { currentRoute } = request; if (currentRoute?.isApi) { // We don't need session for api routes. Restful api should be stateless next(); } else if (currentRoute?.isAdmin) { adminSessionMiddleware(request, response, next); } else { frontStoreSessionMiddleware(request, response, next); } }; app.use(sessionMiddleware); app.use(async (request, response, next) => { // Get the request path, remove '/' from both ends const path = request.originalUrl.split('?')[0].replace(/^\/|\/$/g, ''); // If the current route is already set, or the path contains .hot-update.json, .hot-update.js skip this middleware if (request.currentRoute || path.includes('.hot-update')) { return next(); } // Also skip if we are running in the test mode if (process.env.NODE_ENV === 'test') { return next(); } // Find the matched rewrite rule base on the request path const rewriteRule = await select() .from('url_rewrite') .where('request_path', '=', `/${path}`) .load(pool); if (rewriteRule) { // Find the route const routes = getRoutes(); const route = routes.find((r) => { const regexp = pathToRegexp(r.path); const match = regexp.exec(rewriteRule.target_path); if (match) { request.locals = request.locals || {}; request.locals.customParams = {}; const keys: any[] = []; pathToRegexp(r.path, keys); keys.forEach((key, index) => { request.locals.customParams[key.name] = match[index + 1]; }); return true; } return false; }); // Get the current http method const method = request.method.toUpperCase(); // Check if the route supports the current http method if (route && route.method.includes(method)) { request.currentRoute = route; } return next(); } else { return next(); } }); if (isDevelopmentMode()) { // Admin webpack dev middleware - only for /backend/* paths app.use((request, response, next) => { if (request.path.startsWith('/backend/')) { const adminDevMiddleware = getDevMiddleware(true); adminDevMiddleware.waitUntilValid(() => { const { stats } = adminDevMiddleware.context; if (stats) { response.locals.jsonWebpackStats = stats.toJson(); } }); adminDevMiddleware(request, response, next); } else { next(); } }); app.use((request, response, next) => { if (request.path.startsWith('/__webpack_hmr_admin')) { const adminHotMiddleware = getHotMiddleware(true); adminHotMiddleware(request, response, next); } else { next(); } }); // Frontstore webpack dev middleware - for all other paths app.use((request, response, next) => { if ( !request.path.startsWith('/backend/') && !request.path.startsWith('/__webpack_hmr_admin') ) { const frontstoreDevMiddleware = getDevMiddleware(false); frontstoreDevMiddleware.waitUntilValid(() => { const { stats } = frontstoreDevMiddleware.context; if (stats) { response.locals.jsonWebpackStats = stats.toJson(); } }); frontstoreDevMiddleware(request, response, next); } else { next(); } }); app.use((request, response, next) => { if (request.path.startsWith('/__webpack_hmr_frontstore')) { const frontstoreHotMiddleware = getHotMiddleware(false); frontstoreHotMiddleware(request, response, next); } else { next(); } }); } /** 404 Not Found handle */ app.use((request, response, next) => { if (!request.currentRoute) { response.status(404); const routes = getRoutes(); request.currentRoute = routes.find((r) => r.id === 'notFound'); setPageMetaInfo(request, { title: translate('Not found'), description: translate('Not found') }); next(); } else { next(); } }); } ================================================ FILE: packages/evershop/src/bin/lib/app.js ================================================ import express from 'express'; import { error } from '../../lib/log/logger.js'; import { Handler } from '../../lib/middleware/Handler.js'; import { getModuleMiddlewares } from '../../lib/middleware/index.js'; import { loadModuleRoutes } from '../../lib/router/loadModuleRoutes.js'; import { getRoutes } from '../../lib/router/Router.js'; import { getEnabledExtensions } from '../extension/index.js'; import { addDefaultMiddlewareFuncs } from './addDefaultMiddlewareFuncs.js'; import { getCoreModules } from './loadModules.js'; export const createApp = () => { /** Create express app */ const app = express(); // Enable trust proxy app.enable('trust proxy'); /* Loading modules and initilize routes, components and services */ const modules = getCoreModules(); // Load routes and middleware functions modules.forEach((module) => { try { // Load middleware functions getModuleMiddlewares(module.path); // Load routes loadModuleRoutes(module.path); } catch (e) { error(e); process.exit(0); } }); /** Load extensions */ const extensions = getEnabledExtensions(); extensions.forEach((extension) => { try { // Load middleware functions getModuleMiddlewares(extension.path); // Load routes loadModuleRoutes(extension.path); } catch (e) { error(e); process.exit(0); } }); // Adding default middlewares addDefaultMiddlewareFuncs(app); const routes = getRoutes(); routes.forEach((route) => { // app.all(route.path, Handler.middleware()); route.method.forEach((method) => { switch (method.toUpperCase()) { case 'GET': app.get(route.path, Handler.middleware()); break; case 'POST': app.post(route.path, Handler.middleware()); break; case 'PUT': app.put(route.path, Handler.middleware()); break; case 'DELETE': app.delete(route.path, Handler.middleware()); break; case 'PATCH': app.patch(route.path, Handler.middleware()); break; default: app.get(route.path, Handler.middleware()); break; } }); }); app.use(Handler.middleware()); return app; }; ================================================ FILE: packages/evershop/src/bin/lib/bootstrap/bootstrap.ts ================================================ import { existsSync } from 'fs'; import path from 'path'; import { pathToFileURL } from 'url'; interface Module { path: string; } export type BootstrapContext = { command?: string; env?: 'production' | 'development' | 'test'; process?: 'main' | 'cronjob' | 'event'; }; type BootstrapModule = { default: (context: BootstrapContext) => Promise | void; }; /** * Loads and runs the bootstrap script from a module directory. */ export const loadBootstrapScript = async function loadBootstrapScript( module: Module, context: BootstrapContext = {} ): Promise { const filePath = path.resolve(module.path, 'bootstrap.js'); if (!existsSync(filePath)) { return; } // Convert path to a URL const bootstrapPath = pathToFileURL(filePath).toString(); const bootstrap = (await import(bootstrapPath)) as BootstrapModule; if (typeof bootstrap.default !== 'function') { throw new Error( 'Bootstrap script must provide a default export as a function' ); } await bootstrap.default(context); }; ================================================ FILE: packages/evershop/src/bin/lib/bootstrap/migrate.js ================================================ import { existsSync, readdirSync } from 'fs'; import path from 'path'; import { pathToFileURL } from 'url'; import { commit, insertOnUpdate, rollback, select, startTransaction } from '@evershop/postgres-query-builder'; import semver from 'semver'; import { error } from '../../../lib/log/logger.js'; import { getConnection, pool } from '../../../lib/postgres/connection.js'; import { createMigrationTable } from '../../install/createMigrationTable.js'; async function getCurrentInstalledVersion(module, connection = null) { /** Check for current installed version */ const check = await select() .from('migration') .where('module', '=', module) .load(connection || pool); if (!check) { return '0.0.1'; } else { return check.version; } } async function migrateModule(module, connection = null) { /** Check if the module has migration folder, if not ignore it */ if (!existsSync(path.resolve(module.path, 'migration'))) { return; } const migrations = readdirSync(path.resolve(module.path, 'migration'), { withFileTypes: true }) .filter( (dirent) => dirent.isFile() && dirent.name.match(/^Version-+([1-9].[0-9].[0-9])+.js$/) ) .map((dirent) => dirent.name.replace('Version-', '').replace('.js', '')) .sort((first, second) => semver.lt(first, second)); const currentInstalledVersion = await getCurrentInstalledVersion( module.name, connection ); for (const version of migrations) { /** If the version is lower or equal the installed version, ignore it */ if (semver.lte(version, currentInstalledVersion)) { continue; } const migrationConnection = connection || (await getConnection()); if (!connection) { await startTransaction(migrationConnection); } /** We expect the migration script to provide a function as a default export */ try { const versionModule = await import( pathToFileURL( path.resolve(module.path, 'migration', `Version-${version}.js`) ) ); await versionModule.default(migrationConnection); await insertOnUpdate('migration', ['module']) .given({ module: module.name, version }) .execute(migrationConnection, false); if (!connection) { await commit(migrationConnection); } } catch (e) { if (!connection) { await rollback(migrationConnection); } throw new Error( `Migration failed for module ${module.name}, version ${version}\n${e}` ); } } } export async function migrate(modules, connection = null) { try { const psqlConnection = connection || (await getConnection()); // Create a migration table if not exists. This is for the first time installation await createMigrationTable(psqlConnection); for (const module of modules) { await migrateModule(module, connection); } } catch (e) { error(e); process.exit(0); } } ================================================ FILE: packages/evershop/src/bin/lib/buildEntry.js ================================================ import fs from 'fs'; import { mkdir, writeFile } from 'fs/promises'; import path from 'path'; import { pathToFileURL } from 'url'; import { inspect } from 'util'; import JSON5 from 'json5'; import { getComponentsByRoute } from '../../lib/componee/getComponentsByRoute.js'; import { CONSTANTS } from '../../lib/helpers.js'; import { error } from '../../lib/log/logger.js'; import { generateComponentKey } from '../../lib/util/keyGenerator.js'; import { getRouteBuildPath } from '../../lib/webpack/getRouteBuildPath.js'; import { parseGraphql } from '../../lib/webpack/util/parseGraphql.js'; import { getEnabledWidgets } from '../../lib/widget/widgetManager.js'; /** * Only pass the page routes, not api routes */ export async function buildEntry(routes, clientOnly = false) { const widgets = getEnabledWidgets(); await Promise.all( routes.map(async (route) => { const imports = []; const subPath = getRouteBuildPath(route); const components = getComponentsByRoute(route); if (!components) { return; } /** Build layout and query */ const areas = {}; components.forEach((module) => { if (!fs.existsSync(module)) { return; } const source = fs.readFileSync(module, 'utf8'); // Regex matching 'export const layout = { ... }' const layoutRegex = /export\s+const\s+layout\s*=\s*{\s*areaId\s*:\s*['"]([^'"]+)['"],\s*sortOrder\s*:\s*(\d+)\s*,*\s*}/; const match = source.match(layoutRegex); if (match) { // Remove everything before '{' from the beginning of the match const check = match[0] .replace(/^[^{]*/, '') .replace(/(['"])?([a-zA-Z0-9_]+)(['"])?:/g, '"$2": '); try { const layout = JSON5.parse(check); const id = generateComponentKey(module); const url = pathToFileURL(module).toString(); imports.push(`import ${id} from '${url}';`); areas[layout.areaId] = areas[layout.areaId] || {}; areas[layout.areaId][id] = { id, sortOrder: layout.sortOrder, component: { default: `---${id}---` } }; } catch (e) { error(`Error parsing layout from ${module}`); error(e); } } }); let contentClient = ` import React from 'react'; import ReactDOM from 'react-dom'; import { Area } from '@evershop/evershop/components/common'; import {${ route.isAdmin ? 'HydrateAdmin' : 'HydrateFrontStore' }} from '@evershop/evershop/components/common'; `; areas['*'] = areas['*'] || {}; widgets.forEach((widget) => { const url = route.isAdmin ? pathToFileURL(widget.settingComponent).toString() : pathToFileURL(widget.component).toString(); const id = generateComponentKey( route.isAdmin ? `admin_widget_${widget.type}` : `widget_${widget.type}` ); imports.push(`import ${id} from '${url}';`); areas['*'][id] = { id, sortOrder: widget.sortOrder || 0, component: { default: `---${id}---` } }; }); contentClient += '\r\n'; contentClient += imports.join('\r\n'); contentClient += '\r\n'; contentClient += `Area.defaultProps.components = ${inspect(areas, { depth: 5 }) .replace(/"---/g, '') .replace(/---"/g, '') .replace(/'---/g, '') .replace(/---'/g, '')} `; contentClient += '\r\n'; contentClient += `ReactDOM.hydrate( ${ route.isAdmin ? 'React.createElement(HydrateAdmin, null)' : 'React.createElement(HydrateFrontStore, null)' }, document.getElementById('app') );`; if (!fs.existsSync(path.resolve(subPath, 'client'))) { await mkdir(path.resolve(subPath, 'client'), { recursive: true }); } await writeFile( path.resolve(subPath, 'client', 'entry.js'), contentClient ); if (!clientOnly) { /** Build query */ const query = `${JSON.stringify(parseGraphql(components))}`; // Loop through the widgets config and add the query to the widgets let contentServer = `import React from 'react'; `; contentServer += '\r\n'; contentServer += `import ReactDOM from 'react-dom'; `; contentServer += '\r\n'; contentServer += `import { Area } from '@evershop/evershop/components/common';`; contentServer += '\r\n'; contentServer += `import { renderHtml } from '@evershop/evershop/components/common';\r\n`; contentServer += imports.join('\r\n'); contentServer += '\r\n'; contentServer += `export default renderHtml;\r\n`; contentServer += `Area.defaultProps.components = ${inspect(areas, { depth: 5 }) .replace(/"---/g, '') .replace(/---"/g, '') .replace(/'---/g, '') .replace(/---'/g, '')} `; if (!fs.existsSync(path.resolve(subPath, 'server'))) { await mkdir(path.resolve(subPath, 'server'), { recursive: true }); } await writeFile( path.resolve(subPath, 'server', 'entry.js'), contentServer ); await writeFile( path.resolve(subPath, 'server', 'query.graphql'), query ); } }) ); } ================================================ FILE: packages/evershop/src/bin/lib/devEnvHelper.ts ================================================ import webpack from 'webpack'; import middleware from 'webpack-dev-middleware'; import webpackHotMiddleware from 'webpack-hot-middleware'; import { createConfigClient } from '../../lib/webpack/dev/createConfigClient.js'; type DevConfig = { admin: { compiler?: webpack.Compiler; devMiddleware?: ReturnType; hotMiddleware?: ReturnType; }; frontStore: { compiler?: webpack.Compiler | null; devMiddleware?: ReturnType | null; hotMiddleware?: ReturnType | null; }; }; const webpackConfig = { admin: {}, frontStore: {} } as DevConfig; function getWebpackCompiler(isAdmin: boolean) { const area = isAdmin ? 'admin' : 'frontStore'; if (!webpackConfig[area].compiler) { webpackConfig[area].compiler = webpack(createConfigClient(isAdmin) as any); } return webpackConfig[area].compiler; } function getDevMiddleware(isAdmin: boolean) { const area = isAdmin ? 'admin' : 'frontStore'; if (!webpackConfig[area].devMiddleware) { const compiler = getWebpackCompiler(isAdmin); const devMiddleware = middleware(compiler, { serverSideRender: true, publicPath: isAdmin ? '/backend/' : '/', stats: 'none' }); devMiddleware.context.logger.info = () => {}; webpackConfig[area].devMiddleware = devMiddleware; } return webpackConfig[area].devMiddleware; } function getHotMiddleware(isAdmin: boolean) { const area = isAdmin ? 'admin' : 'frontStore'; if (!webpackConfig[area].hotMiddleware) { const compiler = getWebpackCompiler(isAdmin); const hotMiddleware = webpackHotMiddleware(compiler, { path: isAdmin ? `/__webpack_hmr_admin` : `/__webpack_hmr_frontstore` }); webpackConfig[area].hotMiddleware = hotMiddleware; } return webpackConfig[area].hotMiddleware; } export { getWebpackCompiler, getDevMiddleware, getHotMiddleware }; ================================================ FILE: packages/evershop/src/bin/lib/loadModules.js ================================================ import { readdirSync } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const coreModules = [ { name: 'auth', resolve: path.resolve(__dirname, '../../modules/auth'), path: path.resolve(__dirname, '../../modules/auth') }, { name: 'base', resolve: path.resolve(__dirname, '../../modules/base'), path: path.resolve(__dirname, '../../modules/base') }, { name: 'catalog', resolve: path.resolve(__dirname, '../../modules/catalog'), path: path.resolve(__dirname, '../../modules/catalog') }, { name: 'checkout', resolve: path.resolve(__dirname, '../../modules/checkout'), path: path.resolve(__dirname, '../../modules/checkout') }, { name: 'cms', resolve: path.resolve(__dirname, '../../modules/cms'), path: path.resolve(__dirname, '../../modules/cms') }, { name: 'cod', resolve: path.resolve(__dirname, '../../modules/cod'), path: path.resolve(__dirname, '../../modules/cod') }, { name: 'customer', resolve: path.resolve(__dirname, '../../modules/customer'), path: path.resolve(__dirname, '../../modules/customer') }, { name: 'graphql', resolve: path.resolve(__dirname, '../../modules/graphql'), path: path.resolve(__dirname, '../../modules/graphql') }, { name: 'oms', resolve: path.resolve(__dirname, '../../modules/oms'), path: path.resolve(__dirname, '../../modules/oms') }, { name: 'paypal', resolve: path.resolve(__dirname, '../../modules/paypal'), path: path.resolve(__dirname, '../../modules/paypal') }, { name: 'promotion', resolve: path.resolve(__dirname, '../../modules/promotion'), path: path.resolve(__dirname, '../../modules/promotion') }, { name: 'setting', resolve: path.resolve(__dirname, '../../modules/setting'), path: path.resolve(__dirname, '../../modules/setting') }, { name: 'stripe', resolve: path.resolve(__dirname, '../../modules/stripe'), path: path.resolve(__dirname, '../../modules/stripe') }, { name: 'tax', resolve: path.resolve(__dirname, '../../modules/tax'), path: path.resolve(__dirname, '../../modules/tax') } ]; export function loadModule(path) { return readdirSync(path, { withFileTypes: true }) .filter((dirent) => dirent.isDirectory()) .map((dirent) => ({ name: dirent.name, path: path.resolve(path, dirent.name) })); } export function getCoreModules() { return coreModules; } ================================================ FILE: packages/evershop/src/bin/lib/normalizePort.js ================================================ /** * Normalize a port into a number, string, or false. */ export function normalizePort() { const port = parseInt(process.env.PORT, 10); if (isNaN(port)) { return 3000; } if (port >= 0) { // port number return port; } return 3000; } ================================================ FILE: packages/evershop/src/bin/lib/onError.js ================================================ import { error } from '../../lib/log/logger.js'; import { normalizePort } from './normalizePort.js'; const port = normalizePort(); /** * Event listener for HTTP server "err" event. */ export function onError(err) { if (err.syscall !== 'listen') { throw err; } const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`; // handle specific listen errors with friendly messages switch (err.code) { case 'EACCES': error(`${bind} requires elevated privileges\n`); process.exit(1); break; case 'EADDRINUSE': error(`${bind} is already in use\n`); process.exit(1); break; default: throw err; } } ================================================ FILE: packages/evershop/src/bin/lib/onListening.js ================================================ import boxen from 'boxen'; import kleur from 'kleur'; import { normalizePort } from './normalizePort.js'; const port = normalizePort(); /** * Event listener for HTTP server "listening" event. */ export function onListening() { const message = boxen( `Your website is running at "http://localhost:${port}"`, { title: 'EverShop', titleAlignment: 'center', padding: 1, margin: 1, borderColor: 'green' } ); // eslint-disable-next-line no-console console.log(kleur.green(message)); } ================================================ FILE: packages/evershop/src/bin/lib/prepare.js ================================================ import { getAdminRoutes } from '../../src/lib/router/Router.js'; export function prepare(app, middlewares, routes) { const adminRoutes = getAdminRoutes(); middlewares.forEach((m) => { if (m.routeId === null) { app.use(m.middleware); } else if (m.routeId === 'admin') { adminRoutes.forEach((route) => { if (route.id !== 'adminStaticAsset' || m.id === 'isAdmin') { route.method.forEach((method) => { switch (method.toUpperCase()) { case 'GET': app.get(route.path, m.middleware); break; case 'POST': app.post(route.path, m.middleware); break; case 'PUT': app.put(route.path, m.middleware); break; case 'DELETE': app.delete(route.path, m.middleware); break; default: app.get(route.path, m.middleware); break; } }); } }); } else if (m.routeId === 'frontStore') { app.all('*', (request, response, next) => { const route = request.currentRoute; if (route.isAdmin === true || route.id === 'staticAsset') { return next(); } return m.middleware(request, response, next); }); } else { const route = routes.find((r) => r.id === m.routeId); if (route !== undefined) { route.method.forEach((method) => { switch (method.toUpperCase()) { case 'GET': app.get(route.path, m.middleware); break; case 'POST': app.post(route.path, m.middleware); break; case 'PUT': app.put(route.path, m.middleware); break; case 'DELETE': app.delete(route.path, m.middleware); break; default: app.get(route.path, m.middleware); break; } }); } } }); } ================================================ FILE: packages/evershop/src/bin/lib/startCronProcess.ts ================================================ import path from 'path'; import { fileURLToPath } from 'url'; import spawn from 'cross-spawn'; import { error } from '../../lib/log/logger.js'; import isDevelopmentMode from '../../lib/util/isDevelopmentMode.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export function startCronProcess(context) { // Spawn the child process to manage scheduled jobs const jobArgs = [path.resolve(__dirname, '../../lib/cronjob/cronjob.js')]; if (isDevelopmentMode() || process.argv.includes('--debug')) { jobArgs.push('--debug'); } const jobChild = spawn('node', jobArgs, { stdio: 'inherit', env: { ...process.env, bootstrapContext: JSON.stringify(context), ALLOW_CONFIG_MUTATIONS: true } }); jobChild.on('error', (err) => { error(`Error spawning job processor: ${err}`); }); jobChild.unref(); return jobChild; } ================================================ FILE: packages/evershop/src/bin/lib/startSubscriberProcess.ts ================================================ import path from 'path'; import { fileURLToPath } from 'url'; import spawn from 'cross-spawn'; import { error } from '../../lib/log/logger.js'; import isDevelopmentMode from '../../lib/util/isDevelopmentMode.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export function startSubscriberProcess(context) { const args = [path.resolve(__dirname, '../../lib/event/event-manager.js')]; if (isDevelopmentMode() || process.argv.includes('--debug')) { args.push('--debug'); } const child = spawn('node', args, { stdio: 'inherit', env: { ...process.env, bootstrapContext: JSON.stringify(context), ALLOW_CONFIG_MUTATIONS: true } }); child.on('error', (err) => { error(`Error spawning event processor: ${err}`); }); child.unref(); return child; } ================================================ FILE: packages/evershop/src/bin/lib/startUp.js ================================================ import http from 'http'; import path from 'path'; import { fileURLToPath } from 'url'; import config from 'config'; import spawn from 'cross-spawn'; import { error, debug } from '../../lib/log/logger.js'; import { Handler } from '../../lib/middleware/Handler.js'; import { lockHooks } from '../../lib/util/hookable.js'; import isDevelopmentMode from '../../lib/util/isDevelopmentMode.js'; import { lockRegistry } from '../../lib/util/registry.js'; import { validateConfiguration } from '../../lib/util/validateConfiguration.js'; import { getEnabledExtensions } from '../extension/index.js'; import { createApp } from './app.js'; import { loadBootstrapScript } from './bootstrap/bootstrap.js'; import { migrate } from './bootstrap/migrate.js'; import { getCoreModules } from './loadModules.js'; import { normalizePort } from './normalizePort.js'; import { onError } from './onError.js'; import { onListening } from './onListening.js'; import { startCronProcess } from './startCronProcess.js'; import { startSubscriberProcess } from './startSubscriberProcess.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export const start = async function start(context, cb) { const app = createApp(); /** Create a http server */ const server = http.createServer(app); const modules = [...getCoreModules(), ...getEnabledExtensions()]; /** Loading bootstrap script from modules */ try { for (const module of modules) { await loadBootstrapScript(module, context); } lockHooks(); lockRegistry(); // Get the configuration (nodeconfig) validateConfiguration(config); } catch (e) { error(e); process.exit(0); } process.env.ALLOW_CONFIG_MUTATIONS = false; /** Migration */ try { await migrate(modules); } catch (e) { error(e); process.exit(0); } /** * Get port from environment and store in Express. */ const port = normalizePort(); app.set('port', port); /** Start listening */ server.on('listening', () => { onListening(); if (cb) { cb(); } }); server.on('error', onError); server.listen(port); // Spawn the child process to manage events let subscriberChild = startSubscriberProcess(context); let jobChild = startCronProcess(context); process.on('exit', (code) => { // Cleanup child processes on exit if (subscriberChild && subscriberChild.pid) { subscriberChild.kill('SIGTERM'); } if (jobChild && jobChild.pid) { jobChild.kill('SIGTERM'); } if (code === 100) { debug('Restarting the sever'); process.send('RESTART_ME'); } }); process.on('RESTART_CRONJOB', () => { debug('Restarting the cron job process'); jobChild.kill('SIGTERM'); jobChild = startCronProcess(context); }); process.on('RESTART_SUBSCRIBER', () => { debug('Restarting the subscriber process'); subscriberChild.kill('SIGTERM'); subscriberChild = startSubscriberProcess(context); }); }; ================================================ FILE: packages/evershop/src/bin/lib/watch/broadcast.js ================================================ import { getRoutes } from '../../../lib/router/Router.js'; export const broadcast = async () => { const routes = getRoutes(); routes.forEach((route) => { if (route.hotMiddleware) { const { hotMiddleware } = route; hotMiddleware.publish({ action: 'serverReloaded' }); } }); }; ================================================ FILE: packages/evershop/src/bin/lib/watch/compileSwc.ts ================================================ import fs, { promises as fsp } from 'fs'; import type { PathLike } from 'fs'; import path from 'path'; import { execa } from 'execa'; import { CONSTANTS } from '../../../lib/helpers.js'; import { error, warning } from '../../../lib/log/logger.js'; export async function compileSwc( srcPath: PathLike, distPath: PathLike ): Promise { // Check if the source is a file or directory if (!fs.existsSync(srcPath)) { warning(`Source path ${srcPath} does not exist.`); return; // Check if file extension is not either ts, js, tsx, or jsx } else if ( fs.statSync(srcPath).isFile() && !['.ts', '.js', '.tsx', '.jsx'].includes(path.extname(srcPath as string)) ) { // For this case, we just force copy the file to the dist directory try { const directory = path.dirname(distPath as string); await fsp.mkdir(directory, { recursive: true }); await fsp.copyFile(srcPath as string, distPath as string); } catch (err) { error(`Error copying ${srcPath} to ${distPath}:`); throw err; } } else { let cliOptions; const configFile = path.resolve(CONSTANTS.LIBPATH, '../../.swcrc'); if (fs.statSync(srcPath).isDirectory()) { cliOptions = [ srcPath as string, '-d', distPath as string, '--config-file', configFile, '--strip-leading-paths', '--copy-files' ]; } else { cliOptions = [ srcPath as string, '-o', distPath as string, '--config-file', configFile, '--strip-leading-paths' ]; } try { // Delete the dist directory if it exists using rimraf await fsp.rm(distPath as string, { recursive: true, force: true }); await execa('swc', cliOptions, { cwd: path.resolve(srcPath as string, '..') }); } catch (err) { error(`Error compiling ${srcPath}:`); throw err; } } } ================================================ FILE: packages/evershop/src/bin/lib/watch/effect.ts ================================================ import { existsSync } from 'fs'; import { basename, dirname } from 'path'; import { Application } from 'express'; import { minimatch } from 'minimatch'; import { has } from '../../../bin/dev/register.js'; import { getEnabledJobs } from '../../../lib/cronjob/jobManager.js'; import { debug, error } from '../../../lib/log/logger.js'; import { getRoute } from '../../../lib/router/Router.js'; import { broadcast } from './broadcast.js'; import { isRestartRequired } from './isRestartRequired.js'; import { isSrc } from './isSrc.js'; import { processors } from './processors/index.js'; import { Event } from './watchHandler.js'; export type Effect = | 'restart' | 'restart_cronjob' | 'restart_event' | 'add_middleware' | 'remove_middleware' | 'update_middleware' | 'add_component' | 'remove_component' | 'update_component' | 'add_api_route' | 'remove_api_route' | 'update_api_route' | 'add_admin_route' | 'remove_admin_route' | 'update_admin_route' | 'add_front_store_route' | 'remove_front_store_route' | 'update_front_store_route' | 'update_graphql' | 'unknown'; function isValidRouteFolder(name: string): boolean { const segments = name.split('+'); // Make sure all segment match this regex: /^[a-zA-Z]+$/ return segments.every((segment) => /^[a-zA-Z]+$/.test(segment)); } export function detectEffect(event: Event): Effect { const jobs = getEnabledJobs(); if (isRestartRequired(event)) { return 'restart'; // No specific effect, just a restart required } else if (minimatch(event.path.toString(), '**/*/[A-Z]*.+(jsx|tsx)')) { const routeFolder = basename(dirname(event.path.toString())); if (!isValidRouteFolder(routeFolder)) { return 'unknown'; // Not a valid route folder, skip } if (event.type === 'create') { if ( minimatch( event.path.toString(), '**/pages/+(admin|frontStore)/[A-Z]*.+(jsx|tsx)' ) ) { return 'update_component'; } else { return 'add_component'; } } else if (event.type === 'delete') { return 'remove_component'; } else { return 'update_component'; } } else if (minimatch(event.path.toString(), '**/+(api|admin|frontStore)/*')) { const fileName = basename(event.path.toString()); if (!isValidRouteFolder(fileName)) { return 'unknown'; // Not a valid route folder, skip } if (event.type === 'delete') { const route = getRoute(fileName); if (route) { const routePath = route.path; if (!existsSync(routePath)) { // If the route file does not exist, it means the route folder is deleted. We can safely delete the route. if (route.isApi) { return 'remove_api_route'; } else if (route.isAdmin) { return 'remove_admin_route'; } else { return 'remove_front_store_route'; } } else { return 'remove_middleware'; // The route folder still exists, so we just need to remove the middleware } } else { // This folder is not representing a route, so we just need to take care of midldleware functions return 'remove_middleware'; } } else { return 'unknown'; } } else if ( minimatch( event.path.toString(), '**/+(api|admin|frontStore)/*/[a-z[]*.+(js|ts)' ) ) { const routeFolder = basename(dirname(event.path.toString())); if (!isValidRouteFolder(routeFolder)) { return 'unknown'; // Not a valid route folder, skip } if (event.type === 'create') { return 'add_middleware'; // This is a middleware file } else if (event.type === 'delete') { return 'remove_middleware'; } else { return 'update_middleware'; } } else if (minimatch(event.path.toString(), '**/api/*/route.json')) { const routeFolder = basename(dirname(event.path.toString())); if (!isValidRouteFolder(routeFolder)) { return 'unknown'; // Not a valid route folder, skip } if (event.type === 'create') { return 'add_api_route'; } else if (event.type === 'delete') { return 'remove_api_route'; } else { return 'update_api_route'; } } else if (minimatch(event.path.toString(), '**/api/*/payloadSchema.json')) { // This is a payload schema file for an API route const routeFolder = basename(dirname(event.path.toString())); if (!isValidRouteFolder(routeFolder)) { return 'unknown'; // Not a valid route folder, skip } return 'update_api_route'; } else if (minimatch(event.path.toString(), '**/pages/admin/*/route.json')) { const routeFolder = basename(dirname(event.path.toString())); if (!isValidRouteFolder(routeFolder)) { return 'unknown'; // Not a valid route folder, skip } if (event.type === 'create') { return 'add_admin_route'; } else if (event.type === 'delete') { return 'remove_admin_route'; } else { return 'update_admin_route'; } } else if ( minimatch(event.path.toString(), '**/pages/frontStore/*/route.json') ) { const routeFolder = basename(dirname(event.path.toString())); if (!isValidRouteFolder(routeFolder)) { return 'unknown'; // Not a valid route folder, skip } if (event.type === 'create') { return 'add_front_store_route'; } else if (event.type === 'delete') { return 'remove_front_store_route'; } else { return 'update_front_store_route'; } } else if ( minimatch(event.path.toString(), '**/*/*.graphql') || minimatch(event.path.toString(), '**/*/*.resolvers.+(ts|js)') ) { return 'update_graphql'; // GraphQL schema or resolvers file } else if (minimatch(event.path.toString(), '**/subscribers/**/*.+(ts|js)')) { return 'restart_event'; } // Check if the file is a job file else if ( event.path && jobs.some( (job) => job.resolve === event.path.toString().replace('src', 'dist').replace(/\.ts$/, '.js') ) ) { return 'restart_cronjob'; } else if (isSrc(event.path.toString())) { const distPath = event.path .toString() .replace('src', 'dist') .replace(/\.ts$/, '.js'); if (has(distPath)) { // This module is being used in the application, so we need to restart the process return 'restart'; } else { return 'unknown'; // This is a source file, but not used in the application } } else { const distPath = event.path.toString(); if (has(distPath)) { // This module is being used in the application, so we need to restart the process return 'restart'; } else { return 'unknown'; // This is a source file, but not used in the application } } } export function applyEffects(events: Event[], app: Application) { for (const event of events) { if (!event.effect) { continue; // Skip if no effect is detected } else { const processor = processors[event.effect]; if (processor) { try { debug(`Applying changes: ${event.effect} for ${event.path}`); processor(app, event); } catch (e) { error(`Error applying changes for ${event.path}:`); error(e); } } else { debug(`No processor found for effect type: ${event.effect}`); } } } // Call broadcast to notify all clients about the changes if there are any known effects if ( events.some( (e) => e.effect && !['unknown', 'restart_cronjob', 'restart_event'].includes(e.effect) && !e.effect.includes('component') ) ) { debug('Broadcasting changes to all clients'); broadcast(); } } ================================================ FILE: packages/evershop/src/bin/lib/watch/getDistPaths.ts ================================================ import { PathLike } from 'fs'; export function getDistPaths(): PathLike[] { return ['dist', 'packages/evershop/dist', 'packages/agegate/dist']; } ================================================ FILE: packages/evershop/src/bin/lib/watch/getRootPaths.ts ================================================ import { PathLike } from 'fs'; import path from 'path'; import type { Event } from './watchHandler.js'; /** * Deduplicates a list of paths, keeping only the top-most created folders. * @param {Array<{ path: string, type: string }>} entries * @returns {Event[]} Top-level unique root folders */ export function getRootPaths(entries: Event[]): Event[] { const sortedPaths = entries .map((entry) => path.resolve(entry.path as string)) .sort(); const roots: Event[] = []; for (const current of sortedPaths) { if (!roots.some((root) => current.startsWith(root.path + path.sep))) { roots.push({ path: current, type: entries.find((entry) => entry.path === current)?.type || 'create' }); } } return roots; } ================================================ FILE: packages/evershop/src/bin/lib/watch/getSrcPaths.ts ================================================ import { PathLike } from 'fs'; import path from 'path'; import { getEnabledExtensions } from '../../../bin/extension/index.js'; import { CONSTANTS } from '../../../lib/helpers.js'; import { getEnabledTheme } from '../../../lib/util/getEnabledTheme.js'; export function getSrcPaths(): PathLike[] { const extensions = getEnabledExtensions(); const theme = getEnabledTheme(); return extensions .filter((ext) => ext.srcPath) .map((ext) => ext.srcPath as PathLike) .concat( !CONSTANTS.MODULESPATH.includes('node_modules') ? (path.resolve( CONSTANTS.ROOTPATH, 'packages/evershop/src/' ) as PathLike) : [] ) .concat(theme?.srcPath ? (theme.srcPath as PathLike) : []); } ================================================ FILE: packages/evershop/src/bin/lib/watch/isDist.js ================================================ import path from 'path'; import { getDistPaths } from './getDistPaths.js'; export function isDist(pathName) { if ( getDistPaths().some((distPath) => pathName.startsWith(distPath + path.sep)) ) { return true; } else { return false; } } ================================================ FILE: packages/evershop/src/bin/lib/watch/isRestartRequired.ts ================================================ import path from 'path'; import { CONSTANTS } from '../../../lib/helpers.js'; import { isSrc } from './isSrc.js'; import { Event } from './watchHandler.js'; export function isRestartRequired(event: Event) { if (isSrc(event.path)) { return false; } else if (event.path === path.resolve(CONSTANTS.ROOTPATH, '.env')) { // If the .env file is changed, we need to restart the server return true; } else { const configPath = path.resolve(CONSTANTS.ROOTPATH, 'config'); if ( event.path.toString().startsWith(configPath) && path.extname(event.path as string) === '.json' ) { // If a config JSON file is changed, we need to restart the server return true; } return false; } } ================================================ FILE: packages/evershop/src/bin/lib/watch/isSrc.js ================================================ import path from 'path'; import { getSrcPaths } from './getSrcPaths.js'; export function isSrc(pathName) { if ( getSrcPaths().some((srcPath) => pathName.startsWith(srcPath + path.sep)) ) { return true; } else { return false; } } ================================================ FILE: packages/evershop/src/bin/lib/watch/processors/addAdminRoute.ts ================================================ import { Application } from 'express'; import { warning } from '../../../../lib/log/logger.js'; import { addRoute, hasRoute } from '../../../../lib/router/Router.js'; import { parseRoute } from '../../../../lib/router/scanForRoutes.js'; import { Event } from '../watchHandler.js'; export function addAdminRoute(app: Application, event: Event) { try { const jsonPath = event.path.toString(); const route = parseRoute(jsonPath, true, false); if (hasRoute(route?.id)) { warning(`Route ${route?.id} already exists. Skipping adding new route.`); } else { addRoute(route); } } catch (error) { warning( `Failed to add new route from ${event.path}: ${error.message}. Skipping.` ); } } ================================================ FILE: packages/evershop/src/bin/lib/watch/processors/addApiRoute.ts ================================================ import { Application } from 'express'; import { warning } from '../../../../lib/log/logger.js'; import { Handler } from '../../../../lib/middleware/Handler.js'; import { addRoute, hasRoute } from '../../../../lib/router/Router.js'; import { parseRoute } from '../../../../lib/router/scanForRoutes.js'; import { Event } from '../watchHandler.js'; export function addApiRoute(app: Application, event: Event) { try { const jsonPath = event.path.toString(); const route = parseRoute(jsonPath, false, true); if (!route || hasRoute(route?.id)) { warning(`Route ${route?.id} already exists. Skipping adding new route.`); } else { addRoute(route); for (const method of route.methods) { switch (method.toUpperCase()) { case 'GET': app.get(route.path, Handler.middleware()); break; case 'POST': app.post(route.path, Handler.middleware()); break; case 'PUT': app.put(route.path, Handler.middleware()); break; case 'DELETE': app.delete(route.path, Handler.middleware()); break; case 'PATCH': app.patch(route.path, Handler.middleware()); break; default: app.get(route.path, Handler.middleware()); break; } } } } catch (error) { warning( `Failed to add new route from ${event.path}: ${error.message}. Skipping.` ); } } ================================================ FILE: packages/evershop/src/bin/lib/watch/processors/addComponent.ts ================================================ import { Application } from 'express'; import { warning } from '../../../../lib/log/logger.js'; import { Event } from '../watchHandler.js'; export function addComponent(app: Application, event: Event) { // Do nothing. Let ThemeWatcherPlugin handle this. } ================================================ FILE: packages/evershop/src/bin/lib/watch/processors/addFrontStoreRoute.ts ================================================ import { Application } from 'express'; import { warning } from '../../../../lib/log/logger.js'; import { addRoute, hasRoute } from '../../../../lib/router/Router.js'; import { parseRoute } from '../../../../lib/router/scanForRoutes.js'; import { Event } from '../watchHandler.js'; export function addFrontStoreRoute(app: Application, event: Event) { try { const jsonPath = event.path.toString(); const route = parseRoute(jsonPath, false, false); if (hasRoute(route?.id)) { warning(`Route ${route?.id} already exists. Skipping adding new route.`); } else { addRoute(route); } } catch (error) { warning( `Failed to add new route from ${event.path}: ${error.message}. Skipping.` ); } } ================================================ FILE: packages/evershop/src/bin/lib/watch/processors/addMiddleware.ts ================================================ import { Application } from 'express'; import { warning } from '../../../../lib/log/logger.js'; import { Handler } from '../../../../lib/middleware/Handler.js'; import { Event } from '../watchHandler.js'; export function addMiddleware(app: Application, event: Event) { try { const filePath = event.jsPath?.toString(); Handler.addMiddlewareFromPath(filePath); } catch (error) { warning( `Failed to add new middleware from ${event.jsPath}: ${error.message}. Skipping.` ); } } ================================================ FILE: packages/evershop/src/bin/lib/watch/processors/deleteARoute.ts ================================================ import { basename, dirname } from 'path'; import { Application } from 'express'; import { warning } from '../../../../lib/log/logger.js'; import { deleteRoute, hasRoute } from '../../../../lib/router/Router.js'; import { Event } from '../watchHandler.js'; export function deleteARoute(app: Application, event: Event) { try { const jsonPath = event.path.toString(); const routeId = jsonPath.includes('route.json') ? basename(dirname(jsonPath)) : basename(jsonPath); if (hasRoute(routeId)) { deleteRoute(routeId); } } catch (error) { warning( `Failed to delete route from ${event.path}: ${error.message}. Skipping.` ); } } ================================================ FILE: packages/evershop/src/bin/lib/watch/processors/index.ts ================================================ import { Application } from 'express'; import { Effect } from '../effect.js'; import { addAdminRoute } from './addAdminRoute.js'; import { addApiRoute } from './addApiRoute.js'; import { addComponent } from './addComponent.js'; import { addFrontStoreRoute } from './addFrontStoreRoute.js'; import { addMiddleware } from './addMiddleware.js'; import { deleteARoute } from './deleteARoute.js'; import { removeMiddleware } from './removeMiddleware.js'; import { restartCronJob } from './restartCronJob.js'; import { restartSubscriber } from './restartSubscriber.js'; import { updateAdminRoute } from './updateAdminRoute.js'; import { updateApiRoute } from './updateApiRoute.js'; import { updateFrontStoreRoute } from './updateFrontStoreRoute.js'; export type Processor = { [key in Effect]?: (app: Application, event: any) => void; }; export const processors: Processor = { add_api_route: addApiRoute, update_api_route: updateApiRoute, add_front_store_route: addFrontStoreRoute, update_front_store_route: updateFrontStoreRoute, add_admin_route: addAdminRoute, update_admin_route: updateAdminRoute, remove_api_route: deleteARoute, remove_admin_route: deleteARoute, remove_front_store_route: deleteARoute, add_middleware: addMiddleware, remove_middleware: removeMiddleware, update_middleware: () => {}, update_component: () => { // No operation for update_component, as it is handled by the compiler} }, remove_component: () => { // No operation for update_component, as it is handled by the compiler} }, add_component: addComponent, update_graphql: () => {}, restart_cronjob: () => { restartCronJob(); }, restart_event: () => { restartSubscriber(); } }; ================================================ FILE: packages/evershop/src/bin/lib/watch/processors/removeMiddleware.ts ================================================ import { Application } from 'express'; import { warning } from '../../../../lib/log/logger.js'; import { Handler } from '../../../../lib/middleware/Handler.js'; import { Event } from '../watchHandler.js'; export function removeMiddleware(app: Application, event: Event) { try { const filePath = event.jsPath?.toString(); Handler.removeMiddlewares(filePath); } catch (error) { warning( `Failed to remove middleware from ${event.jsPath}: ${error.message}. Skipping.` ); } } ================================================ FILE: packages/evershop/src/bin/lib/watch/processors/restart.ts ================================================ export function restartProcess() { process.exit(100); } ================================================ FILE: packages/evershop/src/bin/lib/watch/processors/restartCronJob.ts ================================================ export function restartCronJob() { (process as NodeJS.EventEmitter).emit('RESTART_CRONJOB'); } ================================================ FILE: packages/evershop/src/bin/lib/watch/processors/restartSubscriber.ts ================================================ export function restartSubscriber() { (process as NodeJS.EventEmitter).emit('RESTART_SUBSCRIBER'); } ================================================ FILE: packages/evershop/src/bin/lib/watch/processors/touch.js ================================================ import { resolve } from 'path'; import touch from 'touch'; import { CONSTANTS } from '../../../../lib/helpers.js'; export function justATouch(path) { touch( path || resolve( CONSTANTS.MODULESPATH, '../components/common/react/client/Index.js' ) ); } export async function touchList(paths) { await Promise.all(paths.map((p) => touch(p))); } ================================================ FILE: packages/evershop/src/bin/lib/watch/processors/updateAdminRoute.ts ================================================ import { dirname, join } from 'path'; import { Application } from 'express'; import { warning } from '../../../../lib/log/logger.js'; import { addRoute } from '../../../../lib/router/Router.js'; import { parseRoute } from '../../../../lib/router/scanForRoutes.js'; import { Event } from '../watchHandler.js'; export function updateAdminRoute(app: Application, event: Event) { try { const jsonPath = event.path.toString(); const route = parseRoute( join(dirname(jsonPath), 'route.json'), true, false ); addRoute(route); } catch (error) { warning(`Failed to update route from ${event.path}: ${error.message}`); } } ================================================ FILE: packages/evershop/src/bin/lib/watch/processors/updateApiRoute.ts ================================================ import { dirname, join } from 'path'; import { Application } from 'express'; import { warning } from '../../../../lib/log/logger.js'; import { addRoute } from '../../../../lib/router/Router.js'; import { parseRoute } from '../../../../lib/router/scanForRoutes.js'; import { Event } from '../watchHandler.js'; export function updateApiRoute(app: Application, event: Event) { try { const jsonPath = event.path.toString(); const route = parseRoute( join(dirname(jsonPath), 'route.json'), false, true ); addRoute(route); } catch (error) { warning(`Failed to update route from ${event.path}: ${error.message}`); } } ================================================ FILE: packages/evershop/src/bin/lib/watch/processors/updateFrontStoreRoute.ts ================================================ import { dirname, join } from 'path'; import { Application } from 'express'; import { warning } from '../../../../lib/log/logger.js'; import { addRoute } from '../../../../lib/router/Router.js'; import { parseRoute } from '../../../../lib/router/scanForRoutes.js'; import { Event } from '../watchHandler.js'; export function updateFrontStoreRoute(app: Application, event: Event) { try { const jsonPath = event.path.toString(); const route = parseRoute( join(dirname(jsonPath), 'route.json'), false, false ); addRoute(route); } catch (error) { warning(`Failed to update route from ${event.path}: ${error.message}`); } } ================================================ FILE: packages/evershop/src/bin/lib/watch/watchHandler.ts ================================================ import { PathLike, readdirSync, rmSync, statSync } from 'fs'; import path from 'path'; import { Application } from 'express'; import { error } from '../../../lib/log/logger.js'; import { compileSwc } from './compileSwc.js'; import { applyEffects, detectEffect, Effect } from './effect.js'; import { isDist } from './isDist.js'; import { isSrc } from './isSrc.js'; import { restartProcess } from './processors/restart.js'; export type Event = { path: PathLike; type: 'create' | 'update' | 'delete'; jsPath?: PathLike; effect?: Effect; }; export async function watchHandler(events: Event[], app: Application) { if ( events.length === 2 && events.some((e) => e.type === 'delete') && events.some((e) => e.type === 'create') ) { // Likely a rename // Sort the event make sure the delete comes first events.sort((a, b) => (a.type === 'delete' ? -1 : 1)); // Travel the create event and if this is a folder, we need to add create event for every sub-file for (const event of events) { if (event.type === 'create') { // Check if the path is a directory try { const stats = statSync(event.path); if (stats.isDirectory()) { // If it's a directory, we need to add create events for every file in the directory const files = readdirSync(event.path); for (const file of files) { const filePath = path.resolve(event.path as string, file); events.push({ path: filePath, type: 'create' }); } } } catch (e) { error(`Error reading directory ${event.path}:`); error(e); } } } } // Handle the watch event for (const event of events) { event.effect = detectEffect(event); if (event.effect === 'restart') { restartProcess(); break; // Exit the loop if a restart is required, no need to process further } if (isDist(event.path)) { continue; } if (isSrc(event.path)) { const distPath = event.path .toString() .replace('src', 'dist') .replace(/\.ts$/, '.js') .replace(/\.tsx$/, '.js') .replace(/\.jsx$/, '.js'); // Ensure the path ends with .js event.jsPath = distPath; // Set the compiled JS path if (event.type === 'delete') { // Delete whatever is necessary in the dist folder rmSync(distPath as string, { recursive: true, force: true }); } else { // Run swc to compile the files // Get the dist path from the event by replacing the first 'src' with 'dist' and ts to js if this is a ts file await compileSwc(event.path, distPath); } } } applyEffects(events, app); } ================================================ FILE: packages/evershop/src/bin/seed/data/attributes.json ================================================ [ { "attribute_code": "color", "attribute_name": "Color", "type": "select", "is_required": 1, "display_on_frontend": 1, "sort_order": 10, "is_filterable": 1, "groups": [], "options": [ { "option_text": "Black" }, { "option_text": "White" }, { "option_text": "Red" }, { "option_text": "Blue" }, { "option_text": "Green" }, { "option_text": "Yellow" }, { "option_text": "Pink" }, { "option_text": "Gray" }, { "option_text": "Navy" }, { "option_text": "Beige" } ] }, { "attribute_code": "size", "attribute_name": "Size", "type": "select", "is_required": 1, "display_on_frontend": 1, "sort_order": 20, "is_filterable": 1, "groups": [], "options": [ { "option_text": "XS" }, { "option_text": "S" }, { "option_text": "M" }, { "option_text": "L" }, { "option_text": "XL" }, { "option_text": "XXL" } ] } ] ================================================ FILE: packages/evershop/src/bin/seed/data/categories.json ================================================ [ { "name": "Accessories", "url_key": "accessories", "description": [ { "id": "r__accessories", "columns": [ { "size": 1, "id": "c__accessories", "data": { "time": 1729900000000, "blocks": [ { "id": "acc_block_1", "type": "paragraph", "data": { "text": "Complete your look with our stylish accessories" } } ], "version": "2.31.0" } } ], "size": 1, "className": "md:grid-cols-1" } ], "meta_title": "Fashion Accessories", "meta_description": "Browse our collection of fashion accessories", "meta_keywords": "accessories, fashion accessories, style", "status": 1, "include_in_nav": 1 } ] ================================================ FILE: packages/evershop/src/bin/seed/data/collections.json ================================================ [ { "name": "Featured Products", "code": "homepage", "description": [ { "id": "r__featured", "columns": [ { "size": 1, "id": "c__featured", "data": { "time": 1729900000000, "blocks": [ { "id": "fp_block_1", "type": "paragraph", "data": { "text": "Featured products displayed on the homepage" } } ], "version": "2.31.0" } } ], "size": 1, "className": "md:grid-cols-1" } ] }, { "name": "Summer Collection", "code": "summer-2024", "description": [ { "id": "r__summer", "columns": [ { "size": 1, "id": "c__summer", "data": { "time": 1729900000000, "blocks": [ { "id": "sc_block_1", "type": "paragraph", "data": { "text": "Hot picks for the summer season" } } ], "version": "2.31.0" } } ], "size": 1, "className": "md:grid-cols-1" } ] }, { "name": "Winter Essentials", "code": "winter-essentials", "description": [ { "id": "r__winter", "columns": [ { "size": 1, "id": "c__winter", "data": { "time": 1729900000000, "blocks": [ { "id": "we_block_1", "type": "paragraph", "data": { "text": "Stay warm and stylish this winter" } } ], "version": "2.31.0" } } ], "size": 1, "className": "md:grid-cols-1" } ] }, { "name": "Trending Now", "code": "trending", "description": [ { "id": "r__trending", "columns": [ { "size": 1, "id": "c__trending", "data": { "time": 1729900000000, "blocks": [ { "id": "tn_block_1", "type": "paragraph", "data": { "text": "What's hot and trending right now" } } ], "version": "2.31.0" } } ], "size": 1, "className": "md:grid-cols-1" } ] } ] ================================================ FILE: packages/evershop/src/bin/seed/data/pages.json ================================================ [ { "status": true, "url_key": "about-us", "name": "About Us", "content": [ { "id": "r__about_us", "columns": [ { "size": 1, "id": "c__about_us", "data": { "time": 1729900000000, "blocks": [ { "id": "about_us_h2", "type": "header", "data": { "text": "Welcome to Our Store", "level": 2 } }, { "id": "about_us_p1", "type": "paragraph", "data": { "text": "We are passionate about bringing you high-quality ceramic and stainless steel products that combine functionality with elegant design. Our carefully curated collection features items that enhance your daily life, from morning coffee to home organization." } }, { "id": "about_us_h2", "type": "header", "data": { "text": "Our Mission", "level": 2 } }, { "id": "about_us_img1", "type": "image", "data": { "file": { "url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/banner-one.jpg", "width": 2400, "height": 1200 }, "caption": "Our carefully curated collection", "withBorder": false, "withBackground": false, "stretched": false } }, { "id": "about_us_p2", "type": "paragraph", "data": { "text": "We believe that everyday objects should be both beautiful and practical. That's why we source products that are not only aesthetically pleasing but also durable and functional. Each item in our collection is selected with care to ensure it meets our high standards." } }, { "id": "about_us_h3", "type": "header", "data": { "text": "Quality You Can Trust", "level": 2 } }, { "id": "about_us_p3", "type": "paragraph", "data": { "text": "All our products are made from premium materials - from food-safe ceramics to BPA-free stainless steel. We work directly with manufacturers who share our commitment to quality and sustainability. Whether you're looking for office supplies, drinkware, or home decor, you can trust that every item has been thoughtfully designed and rigorously tested." } }, { "id": "about_us_h4", "type": "header", "data": { "text": "Customer Satisfaction", "level": 2 } }, { "id": "about_us_p4", "type": "paragraph", "data": { "text": "Your satisfaction is our top priority. We offer fast shipping, easy returns, and dedicated customer support to ensure your shopping experience is seamless. If you have any questions about our products or need assistance, our team is always here to help." } } ], "version": "2.31.0" } } ], "size": 1, "className": "md:grid-cols-1" } ], "meta_title": "About Us - Learn More About Our Store", "meta_keywords": "about us, our story, company information", "meta_description": "Learn more about our mission to bring you high-quality ceramic and stainless steel products that combine functionality with elegant design." } ] ================================================ FILE: packages/evershop/src/bin/seed/data/products.json ================================================ [ { "type": "simple", "visibility": true, "status": true, "sku": "CUP-001-WHT", "name": "Ceramic Coffee Cup - White", "url_key": "ceramic-coffee-cup-white", "price": 15, "weight": 300, "meta_title": "Ceramic Coffee Cup - White", "meta_description": "Modern ceramic coffee cup in white color, perfect for your morning coffee", "meta_keywords": "coffee cup, ceramic cup, white cup, drinkware", "description": [ { "id": "r__cup_white", "columns": [ { "size": 1, "id": "c__cup_white", "data": { "time": 1729900000000, "blocks": [ { "id": "cup_white_p1", "type": "paragraph", "data": { "text": "Start your day right with our elegant Ceramic Coffee Cup. Crafted from high-quality ceramic, this cup features a smooth finish and comfortable grip." } }, { "id": "cup_white_p2", "type": "paragraph", "data": { "text": "The classic design makes it perfect for both home and office use. Holds 12oz of your favorite beverage and is both microwave and dishwasher safe." } } ], "version": "2.31.0" } } ], "size": 1, "className": "md:grid-cols-1" } ], "qty": 100, "manage_stock": true, "stock_availability": true, "collections": ["homepage", "best-sellers"], "category": "accessories", "variant_group": "ceramic-coffee-cup", "images": [ { "url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/cup-white.jpg", "isMain": true } ], "attributes": [ { "attribute_code": "color", "value": "White" } ] }, { "type": "simple", "visibility": true, "status": true, "sku": "CUP-001-BLK", "name": "Ceramic Coffee Cup - Black", "url_key": "ceramic-coffee-cup-black", "price": 15, "weight": 300, "meta_title": "Ceramic Coffee Cup - Black", "meta_description": "Modern ceramic coffee cup in black color, perfect for your morning coffee", "meta_keywords": "coffee cup, ceramic cup, black cup, drinkware", "description": [ { "id": "r__cup_black", "columns": [ { "size": 1, "id": "c__cup_black", "data": { "time": 1729900000000, "blocks": [ { "id": "cup_black_p1", "type": "paragraph", "data": { "text": "Start your day right with our elegant Ceramic Coffee Cup. Crafted from high-quality ceramic, this cup features a smooth finish and comfortable grip." } } ], "version": "2.31.0" } } ], "size": 1, "className": "md:grid-cols-1" } ], "qty": 100, "manage_stock": true, "stock_availability": true, "collections": ["homepage", "best-sellers"], "category": "accessories", "variant_group": "ceramic-coffee-cup", "images": [ { "url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/cup-black.jpg", "isMain": true } ], "attributes": [ { "attribute_code": "color", "value": "Black" } ] }, { "type": "simple", "visibility": true, "status": true, "sku": "CUP-001-YEL", "name": "Ceramic Coffee Cup - Yellow", "url_key": "ceramic-coffee-cup-yellow", "price": 15, "weight": 300, "meta_title": "Ceramic Coffee Cup - Yellow", "meta_description": "Modern ceramic coffee cup in yellow color, perfect for your morning coffee", "meta_keywords": "coffee cup, ceramic cup, yellow cup, drinkware", "description": [ { "id": "r__cup_yellow", "columns": [ { "size": 1, "id": "c__cup_yellow", "data": { "time": 1729900000000, "blocks": [ { "id": "cup_yellow_p1", "type": "paragraph", "data": { "text": "Start your day right with our elegant Ceramic Coffee Cup. Crafted from high-quality ceramic, this cup features a smooth finish and comfortable grip." } } ], "version": "2.31.0" } } ], "size": 1, "className": "md:grid-cols-1" } ], "qty": 100, "manage_stock": true, "stock_availability": true, "collections": ["homepage", "best-sellers"], "category": "accessories", "variant_group": "ceramic-coffee-cup", "images": [ { "url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/cup-yellow.jpg", "isMain": true } ], "attributes": [ { "attribute_code": "color", "value": "Yellow" } ] }, { "type": "simple", "visibility": true, "status": true, "sku": "PEN-002-WHT", "name": "Desk Pen Holder - White", "url_key": "desk-pen-holder-white", "price": 12, "weight": 200, "meta_title": "Desk Pen Holder - White", "meta_description": "Stylish desk pen holder to keep your workspace organized", "meta_keywords": "pen holder, desk organizer, office supplies, white", "description": [ { "id": "r__pen_white", "columns": [ { "size": 1, "id": "c__pen_white", "data": { "time": 1729900000000, "blocks": [ { "id": "pen_white_p1", "type": "paragraph", "data": { "text": "Keep your desk tidy and organized with our modern Desk Pen Holder. Features multiple compartments for pens, pencils, scissors, and other office supplies." } }, { "id": "pen_white_p2", "type": "paragraph", "data": { "text": "Made from durable materials with a sleek finish that complements any workspace. Perfect for home office or corporate settings." } } ], "version": "2.31.0" } } ], "size": 1, "className": "md:grid-cols-1" } ], "qty": 150, "manage_stock": true, "stock_availability": true, "collections": ["homepage", "new-arrivals"], "category": "accessories", "variant_group": "desk-pen-holder", "images": [ { "url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/pen-holder-white.jpg", "isMain": true } ], "attributes": [ { "attribute_code": "color", "value": "White" } ] }, { "type": "simple", "visibility": true, "status": true, "sku": "PEN-002-BLK", "name": "Desk Pen Holder - Black", "url_key": "desk-pen-holder-black", "price": 12, "weight": 200, "meta_title": "Desk Pen Holder - Black", "meta_description": "Stylish desk pen holder to keep your workspace organized", "meta_keywords": "pen holder, desk organizer, office supplies, black", "description": [ { "id": "r__pen_black", "columns": [ { "size": 1, "id": "c__pen_black", "data": { "time": 1729900000000, "blocks": [ { "id": "pen_black_p1", "type": "paragraph", "data": { "text": "Keep your desk tidy and organized with our modern Desk Pen Holder. Features multiple compartments for pens, pencils, scissors, and other office supplies." } } ], "version": "2.31.0" } } ], "size": 1, "className": "md:grid-cols-1" } ], "qty": 150, "manage_stock": true, "stock_availability": true, "collections": ["homepage", "new-arrivals"], "category": "accessories", "variant_group": "desk-pen-holder", "images": [ { "url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/pen-holder-black.jpg", "isMain": true } ], "attributes": [ { "attribute_code": "color", "value": "Black" } ] }, { "type": "simple", "visibility": true, "status": true, "sku": "PEN-002-YEL", "name": "Desk Pen Holder - Yellow", "url_key": "desk-pen-holder-yellow", "price": 12, "weight": 200, "meta_title": "Desk Pen Holder - Yellow", "meta_description": "Stylish desk pen holder to keep your workspace organized", "meta_keywords": "pen holder, desk organizer, office supplies, yellow", "description": [ { "id": "r__pen_yellow", "columns": [ { "size": 1, "id": "c__pen_yellow", "data": { "time": 1729900000000, "blocks": [ { "id": "pen_yellow_p1", "type": "paragraph", "data": { "text": "Keep your desk tidy and organized with our modern Desk Pen Holder. Features multiple compartments for pens, pencils, scissors, and other office supplies." } } ], "version": "2.31.0" } } ], "size": 1, "className": "md:grid-cols-1" } ], "qty": 150, "manage_stock": true, "stock_availability": true, "collections": ["homepage", "new-arrivals"], "category": "accessories", "variant_group": "desk-pen-holder", "images": [ { "url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/pen-holder-yellow.jpg", "isMain": true } ], "attributes": [ { "attribute_code": "color", "value": "Yellow" } ] }, { "type": "simple", "visibility": true, "status": true, "sku": "BOWL-003-WHT", "name": "Ceramic Candy Bowl - White", "url_key": "ceramic-candy-bowl-white", "price": 18, "weight": 400, "meta_title": "Ceramic Candy Bowl - White", "meta_description": "Elegant ceramic bowl perfect for candy, snacks, or decorative use", "meta_keywords": "candy bowl, ceramic bowl, serving bowl, white", "description": [ { "id": "r__bowl_white", "columns": [ { "size": 1, "id": "c__bowl_white", "data": { "time": 1729900000000, "blocks": [ { "id": "bowl_white_p1", "type": "paragraph", "data": { "text": "Add a touch of elegance to your table with our Ceramic Candy Bowl. Perfect for serving candy, nuts, or small snacks at parties and gatherings." } }, { "id": "bowl_white_p2", "type": "paragraph", "data": { "text": "The smooth ceramic finish and timeless design make it both functional and decorative. Also great for holding keys, jewelry, or other small items." } } ], "version": "2.31.0" } } ], "size": 1, "className": "md:grid-cols-1" } ], "qty": 80, "manage_stock": true, "stock_availability": true, "collections": ["homepage", "best-sellers"], "category": "accessories", "variant_group": "ceramic-candy-bowl", "images": [ { "url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/bowl-white.jpg", "isMain": true } ], "attributes": [ { "attribute_code": "color", "value": "White" } ] }, { "type": "simple", "visibility": true, "status": true, "sku": "BOWL-003-BLK", "name": "Ceramic Candy Bowl - Black", "url_key": "ceramic-candy-bowl-black", "price": 18, "weight": 400, "meta_title": "Ceramic Candy Bowl - Black", "meta_description": "Elegant ceramic bowl perfect for candy, snacks, or decorative use", "meta_keywords": "candy bowl, ceramic bowl, serving bowl, black", "description": [ { "id": "r__bowl_black", "columns": [ { "size": 1, "id": "c__bowl_black", "data": { "time": 1729900000000, "blocks": [ { "id": "bowl_black_p1", "type": "paragraph", "data": { "text": "Add a touch of elegance to your table with our Ceramic Candy Bowl. Perfect for serving candy, nuts, or small snacks at parties and gatherings." } } ], "version": "2.31.0" } } ], "size": 1, "className": "md:grid-cols-1" } ], "qty": 80, "manage_stock": true, "stock_availability": true, "collections": ["homepage", "best-sellers"], "category": "accessories", "variant_group": "ceramic-candy-bowl", "images": [ { "url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/bowl-black.jpg", "isMain": true } ], "attributes": [ { "attribute_code": "color", "value": "Black" } ] }, { "type": "simple", "visibility": true, "status": true, "sku": "BOWL-003-YEL", "name": "Ceramic Candy Bowl - Yellow", "url_key": "ceramic-candy-bowl-yellow", "price": 18, "weight": 400, "meta_title": "Ceramic Candy Bowl - Yellow", "meta_description": "Elegant ceramic bowl perfect for candy, snacks, or decorative use", "meta_keywords": "candy bowl, ceramic bowl, serving bowl, yellow", "description": [ { "id": "r__bowl_yellow", "columns": [ { "size": 1, "id": "c__bowl_yellow", "data": { "time": 1729900000000, "blocks": [ { "id": "bowl_yellow_p1", "type": "paragraph", "data": { "text": "Add a touch of elegance to your table with our Ceramic Candy Bowl. Perfect for serving candy, nuts, or small snacks at parties and gatherings." } } ], "version": "2.31.0" } } ], "size": 1, "className": "md:grid-cols-1" } ], "qty": 80, "manage_stock": true, "stock_availability": true, "collections": ["homepage", "best-sellers"], "category": "accessories", "variant_group": "ceramic-candy-bowl", "images": [ { "url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/bowl-yellow.jpg", "isMain": true } ], "attributes": [ { "attribute_code": "color", "value": "Yellow" } ] }, { "type": "simple", "visibility": true, "status": true, "sku": "VASE-004-WHT", "name": "Modern Ceramic Vase - White", "url_key": "modern-ceramic-vase-white", "price": 25, "weight": 500, "meta_title": "Modern Ceramic Vase - White", "meta_description": "Contemporary ceramic vase perfect for fresh or dried flowers", "meta_keywords": "vase, ceramic vase, flower vase, white, home decor", "description": [ { "id": "r__vase_white", "columns": [ { "size": 1, "id": "c__vase_white", "data": { "time": 1729900000000, "blocks": [ { "id": "vase_white_p1", "type": "paragraph", "data": { "text": "Elevate your home decor with our Modern Ceramic Vase. The sleek, contemporary design complements any interior style, from minimalist to traditional." } }, { "id": "vase_white_p2", "type": "paragraph", "data": { "text": "Perfect for displaying fresh flowers, dried arrangements, or as a standalone decorative piece. The sturdy ceramic construction ensures long-lasting beauty." } } ], "version": "2.31.0" } } ], "size": 1, "className": "md:grid-cols-1" } ], "qty": 60, "manage_stock": true, "stock_availability": true, "collections": ["homepage", "new-arrivals"], "category": "accessories", "variant_group": "modern-ceramic-vase", "images": [ { "url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/vase-white.jpg", "isMain": true } ], "attributes": [ { "attribute_code": "color", "value": "White" } ] }, { "type": "simple", "visibility": true, "status": true, "sku": "VASE-004-BLK", "name": "Modern Ceramic Vase - Black", "url_key": "modern-ceramic-vase-black", "price": 25, "weight": 500, "meta_title": "Modern Ceramic Vase - Black", "meta_description": "Contemporary ceramic vase perfect for fresh or dried flowers", "meta_keywords": "vase, ceramic vase, flower vase, black, home decor", "description": [ { "id": "r__vase_black", "columns": [ { "size": 1, "id": "c__vase_black", "data": { "time": 1729900000000, "blocks": [ { "id": "vase_black_p1", "type": "paragraph", "data": { "text": "Elevate your home decor with our Modern Ceramic Vase. The sleek, contemporary design complements any interior style, from minimalist to traditional." } } ], "version": "2.31.0" } } ], "size": 1, "className": "md:grid-cols-1" } ], "qty": 60, "manage_stock": true, "stock_availability": true, "collections": ["homepage", "new-arrivals"], "category": "accessories", "variant_group": "modern-ceramic-vase", "images": [ { "url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/vase-black.jpg", "isMain": true } ], "attributes": [ { "attribute_code": "color", "value": "Black" } ] }, { "type": "simple", "visibility": true, "status": true, "sku": "VASE-004-YEL", "name": "Modern Ceramic Vase - Green", "url_key": "modern-ceramic-vase-green", "price": 25, "weight": 500, "meta_title": "Modern Ceramic Vase - Green", "meta_description": "Contemporary ceramic vase perfect for fresh or dried flowers", "meta_keywords": "vase, ceramic vase, flower vase, green, home decor", "description": [ { "id": "r__vase_green", "columns": [ { "size": 1, "id": "c__vase_green", "data": { "time": 1729900000000, "blocks": [ { "id": "vase_green_p1", "type": "paragraph", "data": { "text": "Elevate your home decor with our Modern Ceramic Vase. The sleek, contemporary design complements any interior style, from minimalist to traditional." } } ], "version": "2.31.0" } } ], "size": 1, "className": "md:grid-cols-1" } ], "qty": 60, "manage_stock": true, "stock_availability": true, "collections": ["homepage", "new-arrivals"], "category": "accessories", "variant_group": "modern-ceramic-vase", "images": [ { "url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/vase-green.jpg", "isMain": true } ], "attributes": [ { "attribute_code": "color", "value": "Green" } ] }, { "type": "simple", "visibility": true, "status": true, "sku": "THERMO-005-WHT", "name": "Stainless Steel Thermos - White", "url_key": "stainless-steel-thermos-white", "price": 35, "weight": 350, "meta_title": "Stainless Steel Thermos - White", "meta_description": "Insulated stainless steel thermos keeps drinks hot or cold for hours", "meta_keywords": "thermos, insulated bottle, water bottle, white, drinkware", "description": [ { "id": "r__thermo_white", "columns": [ { "size": 1, "id": "c__thermo_white", "data": { "time": 1729900000000, "blocks": [ { "id": "thermo_white_p1", "type": "paragraph", "data": { "text": "Keep your beverages at the perfect temperature with our Stainless Steel Thermos. Double-wall vacuum insulation keeps drinks hot for 12 hours or cold for 24 hours." } }, { "id": "thermo_white_p2", "type": "paragraph", "data": { "text": "The leak-proof lid and durable stainless steel construction make it perfect for travel, work, or outdoor activities. BPA-free and easy to clean." } } ], "version": "2.31.0" } } ], "size": 1, "className": "md:grid-cols-1" } ], "qty": 120, "manage_stock": true, "stock_availability": true, "collections": ["homepage", "best-sellers"], "category": "accessories", "variant_group": "stainless-steel-thermos", "images": [ { "url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/thermos-white.jpg", "isMain": true } ], "attributes": [ { "attribute_code": "color", "value": "White" } ] }, { "type": "simple", "visibility": true, "status": true, "sku": "THERMO-005-BLK", "name": "Stainless Steel Thermos - Black", "url_key": "stainless-steel-thermos-black", "price": 35, "weight": 350, "meta_title": "Stainless Steel Thermos - Black", "meta_description": "Insulated stainless steel thermos keeps drinks hot or cold for hours", "meta_keywords": "thermos, insulated bottle, water bottle, black, drinkware", "description": [ { "id": "r__thermo_black", "columns": [ { "size": 1, "id": "c__thermo_black", "data": { "time": 1729900000000, "blocks": [ { "id": "thermo_black_p1", "type": "paragraph", "data": { "text": "Keep your beverages at the perfect temperature with our Stainless Steel Thermos. Double-wall vacuum insulation keeps drinks hot for 12 hours or cold for 24 hours." } } ], "version": "2.31.0" } } ], "size": 1, "className": "md:grid-cols-1" } ], "qty": 120, "manage_stock": true, "stock_availability": true, "collections": ["homepage", "best-sellers"], "category": "accessories", "variant_group": "stainless-steel-thermos", "images": [ { "url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/thermos-black.jpg", "isMain": true } ], "attributes": [ { "attribute_code": "color", "value": "Black" } ] }, { "type": "simple", "visibility": true, "status": true, "sku": "THERMO-005-YEL", "name": "Stainless Steel Thermos - Yellow", "url_key": "stainless-steel-thermos-yellow", "price": 35, "weight": 350, "meta_title": "Stainless Steel Thermos - Yellow", "meta_description": "Insulated stainless steel thermos keeps drinks hot or cold for hours", "meta_keywords": "thermos, insulated bottle, water bottle, yellow, drinkware", "description": [ { "id": "r__thermo_yellow", "columns": [ { "size": 1, "id": "c__thermo_yellow", "data": { "time": 1729900000000, "blocks": [ { "id": "thermo_yellow_p1", "type": "paragraph", "data": { "text": "Keep your beverages at the perfect temperature with our Stainless Steel Thermos. Double-wall vacuum insulation keeps drinks hot for 12 hours or cold for 24 hours." } } ], "version": "2.31.0" } } ], "size": 1, "className": "md:grid-cols-1" } ], "qty": 120, "manage_stock": true, "stock_availability": true, "collections": ["homepage", "best-sellers"], "category": "accessories", "variant_group": "stainless-steel-thermos", "images": [ { "url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/thermos-yellow.jpg", "isMain": true } ], "attributes": [ { "attribute_code": "color", "value": "Yellow" } ] } ] ================================================ FILE: packages/evershop/src/bin/seed/data/widgets.json ================================================ [ { "name": "Main menu", "type": "basic_menu", "route": ["all"], "area": ["headerMiddleLeft"], "sort_order": 1, "settings": { "menus": [ { "id": "hanhk3km0m8nt2b", "url": "#", "name": "Shop", "type": "custom", "uuid": "#", "children": [ { "id": "hanhk3km0m8nt2c", "url": "/accessories", "name": "Accessories", "type": "custom", "uuid": "/accessories" } ] }, { "id": "hanhk3km0m8nt2e", "url": "/page/about-us", "name": "About us", "type": "custom", "uuid": "/page/about-us", "children": [] } ], "isMain": "1", "className": "" }, "status": true }, { "name": "Homepage Slideshow", "type": "simple_slider", "route": ["homepage"], "area": ["content"], "sort_order": 5, "settings": { "dots": true, "arrows": true, "slides": [ { "id": "slide-1", "image": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/banner-one.jpg", "width": 2400, "height": 1200, "subText": "Discover our exquisite collection of ceramic and stainless steel products", "headline": "Premium Quality Products", "buttonLink": "/accessories", "buttonText": "Shop Now", "buttonColor": "#3a3a3a" }, { "id": "slide-2", "image": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/banner-two.jpg", "width": 2400, "height": 1200, "subText": "Elegant designs that enhance your daily life, from morning coffee to home organization", "headline": "Crafted With Care", "buttonLink": "/accessories", "buttonText": "View Collection", "buttonColor": "#3a3a3a" } ], "autoplay": true, "fullWidth": true, "heightType": "auto", "autoplaySpeed": 3000 }, "status": true }, { "name": "Featured Products", "type": "collection_products", "route": ["homepage"], "area": ["content"], "sort_order": 20, "settings": { "count": 4, "collection": "homepage" }, "status": true } ] ================================================ FILE: packages/evershop/src/bin/seed/imageDownloader.ts ================================================ import { createWriteStream, existsSync, mkdirSync } from 'fs'; import http from 'http'; import https from 'https'; import { dirname } from 'path'; import { pipeline } from 'stream/promises'; import { info, warning } from '../../lib/log/logger.js'; /** * Download an image from a URL and save it to a local file */ export async function downloadImage( url: string, outputPath: string ): Promise { return new Promise((resolve, reject) => { // Ensure directory exists const dir = dirname(outputPath); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } const client = url.startsWith('https') ? https : http; const request = client.get(url, (response) => { // Handle redirects if ( response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307 || response.statusCode === 308 ) { const redirectUrl = response.headers.location; if (redirectUrl) { info(` → Following redirect to: ${redirectUrl}`); downloadImage(redirectUrl, outputPath).then(resolve).catch(reject); return; } } if (response.statusCode !== 200) { reject( new Error(`Failed to download: HTTP ${response.statusCode} - ${url}`) ); return; } const fileStream = createWriteStream(outputPath); pipeline(response, fileStream) .then(() => { info(` ✓ Downloaded: ${url} → ${outputPath}`); resolve(outputPath); }) .catch((err) => { reject(new Error(`Failed to save file: ${err.message}`)); }); }); request.on('error', (err) => { reject(new Error(`Download failed: ${err.message}`)); }); request.setTimeout(30000, () => { request.destroy(); reject(new Error('Download timeout')); }); }); } /** * Generate a filename from URL */ export function getFilenameFromUrl(url: string): string { try { const urlObj = new URL(url); // For Unsplash images, extract photo ID if (urlObj.hostname.includes('unsplash.com')) { const photoId = urlObj.pathname.split('/').pop() || 'image'; return `${photoId}.jpg`; } const pathname = urlObj.pathname; const filename = pathname.split('/').pop() || 'image.jpg'; // Ensure it has an extension if (!filename.includes('.')) { return `${filename}.jpg`; } return filename; } catch { return `image-${Date.now()}.jpg`; } } /** * Convert GitHub raw URL to a local media path */ export function convertToMediaPath(localPath: string): string { // Convert absolute path to relative media path // e.g., /path/to/media/widgets/slide-1.jpg -> /assets/widgets/slide-1.jpg // or on Windows: C:\path\to\media\widgets\slide-1.jpg -> /assets/widgets/slide-1.jpg // Normalize to forward slashes for consistent matching const normalizedPath = localPath.replace(/\\/g, '/'); const mediaMatch = normalizedPath.match(/media\/(.+)$/); if (mediaMatch) { return `/assets/${mediaMatch[1]}`; } return localPath; } ================================================ FILE: packages/evershop/src/bin/seed/index.ts ================================================ /* eslint-disable no-console */ import './initEnvDev.js'; import 'dotenv/config'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { error, success, info } from '../../lib/log/logger.js'; import { seedAttributeGroup, seedAttributes } from './seedAttributes.js'; import { seedCategories } from './seedCategories.js'; import { seedCollections } from './seedCollections.js'; import { seedPages } from './seedPages.js'; import { seedProducts } from './seedProducts.js'; import { seedWidgets } from './seedWidgets.js'; const { argv } = yargs(hideBin(process.argv)) .option('attributes', { alias: 'a', description: 'Seed product attributes', type: 'boolean', default: false }) .option('categories', { alias: 'c', description: 'Seed categories', type: 'boolean', default: false }) .option('collections', { alias: 'col', description: 'Seed collections', type: 'boolean', default: false }) .option('products', { alias: 'p', description: 'Seed products', type: 'boolean', default: false }) .option('widgets', { alias: 'w', description: 'Seed widgets', type: 'boolean', default: false }) .option('pages', { alias: 'pg', description: 'Seed CMS pages', type: 'boolean', default: false }) .option('all', { description: 'Seed all demo data (attributes, categories, collections, products, widgets, pages)', type: 'boolean', default: false }) .check((argv) => { if ( !argv.attributes && !argv.categories && !argv.collections && !argv.products && !argv.widgets && !argv.pages && !argv.all ) { throw new Error( 'Please specify at least one option: --attributes, --categories, --collections, --products, --widgets, --pages, or --all' ); } return true; }) .help(); interface SeedOptions { attributes: boolean; categories: boolean; collections: boolean; products: boolean; widgets: boolean; pages: boolean; all: boolean; } async function seed() { const options = argv as unknown as SeedOptions; let demoAttributeGroupId: number | null = null; try { info('Starting demo data seeding...\n'); // Create attribute group first if we're seeding attributes or products if (options.all || options.attributes || options.products) { demoAttributeGroupId = await seedAttributeGroup(); console.log(); } if (options.all || options.attributes) { if (!demoAttributeGroupId) { demoAttributeGroupId = await seedAttributeGroup(); } await seedAttributes(demoAttributeGroupId); console.log(); } if (options.all || options.categories) { await seedCategories(); console.log(); } if (options.all || options.collections) { await seedCollections(); console.log(); } if (options.all || options.products) { if (!demoAttributeGroupId) { demoAttributeGroupId = await seedAttributeGroup(); } await seedProducts(demoAttributeGroupId); console.log(); } if (options.all || options.widgets) { await seedWidgets(); console.log(); } if (options.all || options.pages) { await seedPages(); console.log(); } success('✓ Demo data seeding completed successfully!'); process.exit(0); } catch (e: any) { error(`Seeding failed: ${e.message}`); process.exit(1); } } seed(); ================================================ FILE: packages/evershop/src/bin/seed/initEnvDev.ts ================================================ import 'dotenv/config'; process.env.NODE_ENV = 'development'; ================================================ FILE: packages/evershop/src/bin/seed/seedAttributes.ts ================================================ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { insert, select } from '@evershop/postgres-query-builder'; import { info, success, error } from '../../lib/log/logger.js'; import { pool } from '../../lib/postgres/connection.js'; import createProductAttribute from '../../modules/catalog/services/attribute/createProductAttribute.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Create or get the demo attribute group */ export async function seedAttributeGroup(): Promise { info('Creating demo attribute group...'); // Check if demo group already exists const existingGroup = await select() .from('attribute_group') .where('group_name', '=', 'Demo Products') .load(pool); if (existingGroup) { info('Demo attribute group already exists, reusing...'); return existingGroup.attribute_group_id; } // Create the demo attribute group const result = await insert('attribute_group') .given({ group_name: 'Demo Products' }) .execute(pool); success(`✓ Created attribute group: Demo Products (ID: ${result.insertId})`); return result.insertId; } /** * Seed product attributes from JSON file */ export async function seedAttributes( demoAttributeGroupId: number ): Promise { info('Seeding attributes...'); const dataPath = path.join(__dirname, 'data', 'attributes.json'); const attributesData = JSON.parse(fs.readFileSync(dataPath, 'utf-8')); for (const attributeData of attributesData) { try { // Check if attribute already exists const existingAttribute = await select() .from('attribute') .where('attribute_code', '=', attributeData.attribute_code) .load(pool); if (existingAttribute) { info( `Attribute "${attributeData.attribute_name}" already exists, updating options...` ); // If attribute has options (select/multiselect type), sync the options if (attributeData.options && Array.isArray(attributeData.options)) { for (const optionData of attributeData.options) { // Check if option already exists const existingOption = await select() .from('attribute_option') .where('attribute_id', '=', existingAttribute.attribute_id) .and('option_text', '=', optionData.option_text) .load(pool); if (!existingOption) { // Add new option - must include attribute_code await insert('attribute_option') .given({ attribute_id: existingAttribute.attribute_id, attribute_code: existingAttribute.attribute_code, option_text: optionData.option_text }) .execute(pool); success(` ✓ Added option: ${optionData.option_text}`); } else { info(` → Option "${optionData.option_text}" already exists`); } } } // Ensure attribute is linked to demo group const existingLink = await select() .from('attribute_group_link') .where('attribute_id', '=', existingAttribute.attribute_id) .and('group_id', '=', demoAttributeGroupId) .load(pool); if (!existingLink) { await insert('attribute_group_link') .given({ attribute_id: existingAttribute.attribute_id, group_id: demoAttributeGroupId }) .execute(pool); info(` → Linked to Demo Products group`); } continue; } // Add the demo group if no groups specified if (!attributeData.groups || attributeData.groups.length === 0) { attributeData.groups = [demoAttributeGroupId]; } await createProductAttribute(attributeData, {}); success(`✓ Created attribute: ${attributeData.attribute_name}`); } catch (e: any) { error( `Failed to create attribute ${attributeData.attribute_name}: ${e.message}` ); } } } ================================================ FILE: packages/evershop/src/bin/seed/seedCategories.ts ================================================ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { select } from '@evershop/postgres-query-builder'; import { info, success, error } from '../../lib/log/logger.js'; import { pool } from '../../lib/postgres/connection.js'; import createCategory from '../../modules/catalog/services/category/createCategory.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Seed categories from JSON file */ export async function seedCategories(): Promise { info('Seeding categories...'); const dataPath = path.join(__dirname, 'data', 'categories.json'); const categoriesData = JSON.parse(fs.readFileSync(dataPath, 'utf-8')); for (const categoryData of categoriesData) { try { // Check if category already exists const existingCategory = await select() .from('category_description') .where('url_key', '=', categoryData.url_key) .load(pool); if (existingCategory) { info(`Category "${categoryData.name}" already exists, skipping...`); continue; } await createCategory(categoryData, {}); success(`✓ Created category: ${categoryData.name}`); } catch (e: any) { error(`Failed to create category ${categoryData.name}: ${e.message}`); } } } ================================================ FILE: packages/evershop/src/bin/seed/seedCollections.ts ================================================ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { select } from '@evershop/postgres-query-builder'; import { info, success, error } from '../../lib/log/logger.js'; import { pool } from '../../lib/postgres/connection.js'; import createCollection from '../../modules/catalog/services/collection/createCollection.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Seed collections from JSON file */ export async function seedCollections(): Promise { info('Seeding collections...'); const dataPath = path.join(__dirname, 'data', 'collections.json'); const collectionsData = JSON.parse(fs.readFileSync(dataPath, 'utf-8')); for (const collectionData of collectionsData) { try { // Check if collection already exists const existingCollection = await select() .from('collection') .where('code', '=', collectionData.code) .load(pool); if (existingCollection) { info(`Collection "${collectionData.name}" already exists, skipping...`); continue; } await createCollection(collectionData, {}); success(`✓ Created collection: ${collectionData.name}`); } catch (e: any) { error(`Failed to create collection ${collectionData.name}: ${e.message}`); } } } ================================================ FILE: packages/evershop/src/bin/seed/seedImages.ts ================================================ import { existsSync, mkdirSync } from 'fs'; import { join } from 'path'; import { insert, select } from '@evershop/postgres-query-builder'; import { CONSTANTS } from '../../lib/helpers.js'; import { info, success, warning, error } from '../../lib/log/logger.js'; import { pool } from '../../lib/postgres/connection.js'; import { downloadImage, getFilenameFromUrl } from './imageDownloader.js'; /** * Seed product images by downloading from GitHub raw URLs */ export async function seedProductImages( productId: number, images: any[] ): Promise { if (!images || images.length === 0) return; for (let i = 0; i < images.length; i++) { const imageData = images[i]; try { let finalImageUrl = imageData.url; // Download image if it's a remote URL if (imageData.url && imageData.url.startsWith('http')) { info(` → Downloading image: ${imageData.url}`); // Get filename from URL const filename = getFilenameFromUrl(imageData.url); // Create local path - organize by SKU const subPath = `catalog/${ Math.floor(Math.random() * (9999 - 1000)) + 1000 }/${Math.floor(Math.random() * (9999 - 1000)) + 1000}`; const mediaDir = join(CONSTANTS.ROOTPATH, 'media', subPath); // Ensure directory exists if (!existsSync(mediaDir)) { mkdirSync(mediaDir, { recursive: true }); } const localPath = join(mediaDir, filename); try { // Download image await downloadImage(imageData.url, localPath); // Convert to media URL finalImageUrl = `/assets/${subPath}/${filename}`; success(` ✓ Downloaded and saved: ${mediaDir}`); // Check if image record already exists const existingImage = await select() .from('product_image') .where('product_image_product_id', '=', productId) .and('origin_image', '=', finalImageUrl) .load(pool); if (!existingImage) { // Save image URL to database await insert('product_image') .given({ product_image_product_id: productId, origin_image: finalImageUrl, is_main: imageData.isMain ? 1 : 0 }) .execute(pool); info(` ✓ Added image record to database`); } else { info(` → Image already exists in database`); } } catch (downloadErr: any) { error(` ✗ Failed to download image: ${downloadErr.message}`); } } } catch (e: any) { warning(` ⚠️ Failed to process image ${i + 1}: ${e.message}`); } } } ================================================ FILE: packages/evershop/src/bin/seed/seedPages.ts ================================================ import { readFileSync } from 'fs'; import { join } from 'path'; import { dirname } from 'path'; import { fileURLToPath } from 'url'; import { insert, select } from '@evershop/postgres-query-builder'; import { error, info, success } from '../../lib/log/logger.js'; import { getConnection } from '../../lib/postgres/connection.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); interface PageData { status: boolean; url_key: string; name: string; content: any[]; meta_title: string; meta_keywords?: string; meta_description?: string; } /** * Seed CMS pages from JSON file */ export async function seedPages(): Promise { try { info('Seeding CMS pages...'); // Read pages data const pagesPath = join(__dirname, 'data', 'pages.json'); const pagesData: PageData[] = JSON.parse(readFileSync(pagesPath, 'utf-8')); const connection = await getConnection(); let created = 0; let skipped = 0; for (const pageData of pagesData) { // Check if page already exists (by url_key) const existing = await select() .from('cms_page_description') .where('url_key', '=', pageData.url_key) .load(connection, false); if (existing) { info(` ⊘ Page "${pageData.url_key}" already exists, skipping...`); skipped++; continue; } // Insert cms_page first const page = await insert('cms_page') .given({ status: pageData.status }) .execute(connection, false); // Insert cms_page_description await insert('cms_page_description') .given({ cms_page_description_cms_page_id: page.cms_page_id, url_key: pageData.url_key, name: pageData.name, content: JSON.stringify(pageData.content), meta_title: pageData.meta_title, meta_keywords: pageData.meta_keywords || null, meta_description: pageData.meta_description || null }) .execute(connection); success(` ✓ Created page: ${pageData.name} (/${pageData.url_key})`); created++; } success( `✓ CMS pages seeding complete: ${created} created, ${skipped} skipped` ); } catch (e: any) { error(`Failed to seed pages: ${e.message}`); throw e; } } ================================================ FILE: packages/evershop/src/bin/seed/seedProducts.ts ================================================ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { insert, select } from '@evershop/postgres-query-builder'; import { info, success, error, warning } from '../../lib/log/logger.js'; import { pool } from '../../lib/postgres/connection.js'; import createProduct from '../../modules/catalog/services/product/createProduct.js'; import { seedProductImages } from './seedImages.js'; import { createVariantGroups, resolveAttributeOptions } from './variantGroupHelpers.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Seed products from JSON file */ export async function seedProducts( demoAttributeGroupId: number ): Promise { info('Seeding products...'); const dataPath = path.join(__dirname, 'data', 'products.json'); const productsData = JSON.parse(fs.readFileSync(dataPath, 'utf-8')); // Get color and size attribute IDs const colorAttribute = await select() .from('attribute') .where('attribute_code', '=', 'color') .load(pool); const sizeAttribute = await select() .from('attribute') .where('attribute_code', '=', 'size') .load(pool); if (!colorAttribute || !sizeAttribute) { error( 'Color and Size attributes must be seeded first. Run: npm run seed -- --attributes' ); return; } // Create variant groups const variantGroupIds = await createVariantGroups( productsData, demoAttributeGroupId, colorAttribute.attribute_id ); // Seed products info('\nSeeding products...'); for (const productData of productsData) { try { // Check if product already exists const existingProduct = await select() .from('product') .where('sku', '=', productData.sku) .load(pool); if (existingProduct) { info( `Product "${productData.name}" (${productData.sku}) already exists, skipping...` ); continue; } // Assign product to the demo attribute group if (!productData.group_id) { if (!demoAttributeGroupId) { error('Demo attribute group ID is not set. This should not happen.'); continue; } productData.group_id = demoAttributeGroupId; } // Resolve category_id from the category field if (productData.category) { const categoryUrlKey = productData.category; const categoryQuery = select('category.category_id').from( 'category_description' ); categoryQuery .leftJoin('category') .on( 'category.category_id', '=', 'category_description.category_description_category_id' ); categoryQuery.where( 'category_description.url_key', '=', categoryUrlKey ); const category = await categoryQuery.load(pool); if (category && category.category_id) { productData.category_id = category.category_id; } else { warning( ` ⚠️ Category "${categoryUrlKey}" not found, product will have no category` ); } // Remove category field as it's not needed for product creation delete productData.category; } // Save collections, images, and variant_group for later processing const collections = productData.collections; const images = productData.images; const variantGroup = productData.variant_group; delete productData.collections; delete productData.images; delete productData.variant_group; // Set variant_group_id if this product belongs to a variant group if (variantGroup && variantGroupIds.has(variantGroup)) { productData.variant_group_id = variantGroupIds.get(variantGroup); info( ` → Assigning to variant group: ${variantGroup} (ID: ${productData.variant_group_id})` ); } // Convert attribute values to option IDs for select type attributes if (productData.attributes && Array.isArray(productData.attributes)) { productData.attributes = await resolveAttributeOptions( productData.attributes ); } const product = await createProduct(productData, {}); success(`✓ Created product: ${productData.name} (${productData.sku})`); // Process images if (images && Array.isArray(images)) { await seedProductImages(product.insertId, images); } // Assign product to collections if specified if (collections && Array.isArray(collections)) { for (const collectionCode of collections) { const collection = await select() .from('collection') .where('code', '=', collectionCode) .load(pool); if (collection) { await insert('product_collection') .given({ collection_id: collection.collection_id, product_id: product.insertId }) .execute(pool); info(` → Assigned to collection: ${collectionCode}`); } } } } catch (e: any) { error(`Failed to create product ${productData.name}: ${e.message}`); } } } ================================================ FILE: packages/evershop/src/bin/seed/seedWidgets.ts ================================================ import { readFileSync, existsSync, mkdirSync } from 'fs'; import { join, resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; import { insert, select } from '@evershop/postgres-query-builder'; import { CONSTANTS } from '../../lib/helpers.js'; import { error, info, success } from '../../lib/log/logger.js'; import { getConnection } from '../../lib/postgres/connection.js'; import { downloadImage, getFilenameFromUrl, convertToMediaPath } from './imageDownloader.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); interface WidgetData { name: string; type: string; status: 1 | 0; area: string; route: string[]; settings: Record; sort_order: number; } interface SlideData { id: string; image: string; width: number; height: number; headline?: string; subheadline?: string; buttonText?: string; buttonUrl?: string; } /** * Download slideshow images and update URLs */ async function downloadSlideshowImages( settings: Record ): Promise> { if (settings.slides && Array.isArray(settings.slides)) { const updatedSlides: SlideData[] = []; for (const slide of settings.slides as SlideData[]) { if (slide.image && slide.image.startsWith('http')) { try { info(` → Downloading slide image: ${slide.image}`); // Get filename from URL const filename = getFilenameFromUrl(slide.image); const slideId = slide.id || `slide-${Date.now()}`; // Create local path const mediaDir = join( CONSTANTS.ROOTPATH, 'media', 'widgets', slideId ); // Ensure directory exists if (!existsSync(mediaDir)) { mkdirSync(mediaDir, { recursive: true }); } const localPath = join(mediaDir, filename); // Download image await downloadImage(slide.image, localPath); // Convert to media URL const mediaUrl = convertToMediaPath(localPath); // Update slide with local URL updatedSlides.push({ ...slide, image: mediaUrl }); } catch (err) { error(` ✗ Failed to download slide image: ${err}`); // Keep original URL on failure updatedSlides.push(slide); } } else { updatedSlides.push(slide); } } return { ...settings, slides: updatedSlides }; } return settings; } /** * Seed widgets from JSON file */ export async function seedWidgets(): Promise { try { info('Seeding widgets...'); // Read widgets data const widgetsPath = join(__dirname, 'data', 'widgets.json'); const widgetsData: WidgetData[] = JSON.parse( readFileSync(widgetsPath, 'utf-8') ); const connection = await getConnection(); let created = 0; let skipped = 0; for (const widgetData of widgetsData) { // Check if widget already exists (by name and type) const existing = await select() .from('widget') .where('name', '=', widgetData.name) .and('type', '=', widgetData.type) .load(connection, false); if (existing) { info(` ⊘ Widget "${widgetData.name}" already exists, skipping...`); skipped++; continue; } // Process settings - download slideshow images if needed let processedSettings = widgetData.settings; if (widgetData.type === 'simple_slider') { info(` → Processing slideshow images for: ${widgetData.name}`); processedSettings = await downloadSlideshowImages(widgetData.settings); } // Insert widget await insert('widget') .given({ name: widgetData.name, type: widgetData.type, area: widgetData.area, route: JSON.stringify(widgetData.route), sort_order: widgetData.sort_order, settings: JSON.stringify(processedSettings), status: widgetData.status }) .execute(connection, false); success(` ✓ Created widget: ${widgetData.name}`); created++; } success( `✓ Widget seeding complete: ${created} created, ${skipped} skipped` ); } catch (e: any) { error(`Failed to seed widgets: ${e.message}`); throw e; } } ================================================ FILE: packages/evershop/src/bin/seed/variantGroupHelpers.ts ================================================ import { insert, select } from '@evershop/postgres-query-builder'; import { v4 as uuidv4 } from 'uuid'; import { info, success, error } from '../../lib/log/logger.js'; import { pool } from '../../lib/postgres/connection.js'; /** * Create variant groups for products */ export async function createVariantGroups( productsData: any[], demoAttributeGroupId: number, colorAttributeId: number ): Promise> { const variantGroupIds = new Map(); // Collect unique variant group names const uniqueGroups = new Set(); for (const productData of productsData) { if (productData.variant_group) { uniqueGroups.add(productData.variant_group); } } // Create variant group records info('Creating variant groups...'); for (const groupName of uniqueGroups) { try { // Generate a proper UUID compatible with PostgreSQL const uuid = uuidv4(); // Create the variant group with attribute IDs and attribute_group_id const result = await insert('variant_group') .given({ uuid: uuid, attribute_group_id: demoAttributeGroupId, attribute_one: colorAttributeId, attribute_two: null, attribute_three: null, attribute_four: null, attribute_five: null, visibility: 1 }) .execute(pool); variantGroupIds.set(groupName, result.insertId); success( `✓ Created variant group: ${groupName} (ID: ${result.insertId}, UUID: ${uuid})` ); } catch (e: any) { error(`Failed to create variant group ${groupName}: ${e.message}`); } } return variantGroupIds; } /** * Resolve attribute option IDs from text values */ export async function resolveAttributeOptions( attributes: any[] ): Promise { const validAttributes: any[] = []; for (const attr of attributes) { // Check the attribute type const attribute = await select() .from('attribute') .where('attribute_code', '=', attr.attribute_code) .load(pool); if ( attribute && (attribute.type === 'select' || attribute.type === 'multiselect') ) { // Look up the option ID by option text const option = await select() .from('attribute_option') .where('attribute_id', '=', attribute.attribute_id) .and('option_text', '=', attr.value) .load(pool); if (option) { // Replace the text value with the option ID attr.value = option.attribute_option_id.toString(); validAttributes.push(attr); info( ` → Resolved ${attr.attribute_code}: "${option.option_text}" → ID ${option.attribute_option_id}` ); } else { error( ` ✗ Option "${attr.value}" not found for attribute "${attr.attribute_code}" - skipping this attribute` ); // Don't add this attribute to validAttributes } } else { // Non-select attributes, add as-is validAttributes.push(attr); } } return validAttributes; } ================================================ FILE: packages/evershop/src/bin/start/index.ts ================================================ import './initEnvStart.js'; import { start } from '../lib/startUp.js'; start({ command: 'start', env: 'production', process: 'main' }); process.on('uncaughtException', function (exception) { import('../../lib/log/logger.js').then((module) => { module.error(exception); }); }); process.on('unhandledRejection', (reason, p) => { import('../../lib/log/logger.js').then((module) => { module.error(`Unhandled Rejection: ${reason} at: ${p}`); }); }); ================================================ FILE: packages/evershop/src/bin/start/initEnvStart.ts ================================================ import 'dotenv/config'; process.env.NODE_ENV = 'production'; process.env.ALLOW_CONFIG_MUTATIONS = 'true'; ================================================ FILE: packages/evershop/src/bin/theme/active.ts ================================================ #!/usr/bin/env node /* eslint-disable no-console */ import { exec } from 'child_process'; import fs from 'fs/promises'; import path from 'path'; import boxen from 'boxen'; import enquirer from 'enquirer'; import kleur from 'kleur'; import ora from 'ora'; const { prompt } = enquirer; async function selectTheme() { const themesDir = path.join(process.cwd(), 'themes'); let themeNames: string[] = []; try { const files = await fs.readdir(themesDir, { withFileTypes: true }); themeNames = files .filter((dirent) => dirent.isDirectory()) .map((dirent) => dirent.name); if (themeNames.length === 0) { console.error(kleur.red('No themes found in themes directory.')); process.exit(1); } } catch (err) { console.error(kleur.red('Error reading themes directory:'), err); process.exit(1); } const response: any = await prompt({ type: 'select', name: 'theme', message: 'Select a theme to activate:', choices: themeNames }); return response.theme; } async function updateConfig(theme: string) { const configDir = path.join(process.cwd(), 'config'); const configPath = path.join(configDir, 'default.json'); try { // Ensure config directory exists try { await fs.access(configDir); } catch { await fs.mkdir(configDir, { recursive: true }); } // Read existing config or create new one let config: any = {}; try { const configData = await fs.readFile(configPath, 'utf8'); config = JSON.parse(configData); } catch (err: any) { // If file doesn't exist, start with empty config if (err.code !== 'ENOENT') { throw err; } } // Update theme config.system = config.system || {}; config.system.theme = theme; await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8'); console.log( boxen(kleur.green(`Theme updated to "${theme}" in config/default.json`), { padding: 1, borderColor: 'green' }) ); } catch (err) { console.error(kleur.red('Error updating config:'), err); process.exit(1); } } async function runBuild() { const spinner = ora('Running build...').start(); return new Promise((resolve, reject) => { exec('npm run build', (error, stdout, stderr) => { if (error) { spinner.fail('Build failed'); console.error(stderr); return reject(error); } else { spinner.succeed('Build completed successfully'); console.log(stdout); return resolve(); } }); }); } async function confirmBuild() { const response: any = await prompt({ type: 'confirm', name: 'runBuild', initial: true, message: 'Would you like to run "npm run build" now?' }); return response.runBuild; } async function activateTheme() { const theme = await selectTheme(); await updateConfig(theme); const shouldBuild = await confirmBuild(); if (shouldBuild) { await runBuild(); } else { console.log( kleur.yellow('Remember to run "npm run build" later to apply changes.') ); } } activateTheme().catch((err) => { console.error(kleur.red('An error occurred:'), err); process.exit(1); }); ================================================ FILE: packages/evershop/src/bin/theme/create.ts ================================================ #!/usr/bin/env node /* eslint-disable no-console */ import fs from 'fs/promises'; import path from 'path'; import enquirer from 'enquirer'; import kleur from 'kleur'; const { prompt } = enquirer; function capitalize(str) { if (!str) return ''; return str.charAt(0).toUpperCase() + str.slice(1); } async function isRealDirectory(path) { try { const stats = await fs.lstat(path); if (stats.isSymbolicLink()) { return false; } return stats.isDirectory(); } catch (err) { if (err.code === 'ENOENT') { return false; } throw err; } } async function createTheme() { const response: any = await prompt({ type: 'input', name: 'name', message: 'Enter new theme name (alphanumeric, dashes or underscores only):' }); const name: string = response.name.trim(); // Validate name if (!/^[A-Za-z0-9_-]+$/.test(name)) { console.error( kleur.red( 'Invalid theme name. Use only letters, numbers, dashes or underscores.' ) ); process.exit(1); } const themeDir = path.join(process.cwd(), 'themes', name); const pagesDir = path.join(themeDir, 'src', 'pages', 'homepage'); const componentFile = path.join(pagesDir, `${capitalize(name)}.tsx`); // Prevent overwriting existing themes try { await fs.access(themeDir); console.error(kleur.red(`Theme '${name}' already exists.`)); process.exit(1); } catch (err: any) { if (err.code !== 'ENOENT') { console.error(kleur.red('Error checking theme existence:'), err); process.exit(1); } // Directory does not exist, proceed } try { // Create directories await fs.mkdir(pagesDir, { recursive: true }); // Create package.json for the new theme const packageJsonPath = path.join(themeDir, 'package.json'); const packageJsonContent = { name, version: '0.1.0', type: 'module', private: true, scripts: { build: 'tsc' } }; await fs.writeFile( packageJsonPath, JSON.stringify(packageJsonContent, null, 2), 'utf8' ); // Create tsconfig.json for the new theme const tsconfigPath = path.join(themeDir, 'tsconfig.json'); const tsconfigContent = { compilerOptions: { module: 'NodeNext', target: 'ES2018', lib: ['dom', 'dom.iterable', 'esnext'], esModuleInterop: true, forceConsistentCasingInFileNames: true, skipLibCheck: true, declaration: true, sourceMap: true, allowJs: true, checkJs: false, jsx: 'react', outDir: './dist', resolveJsonModule: true, allowSyntheticDefaultImports: true, allowArbitraryExtensions: true, strictNullChecks: true, baseUrl: '.', rootDir: 'src', paths: { '@components/*': (await isRealDirectory( path.join(process.cwd(), 'node_modules', '@evershop', 'evershop') )) ? [ './src/components/*', '../../node_modules/@evershop/evershop/src/components/*' ] : ['./src/components/*', '../../packages/evershop/src/components/*'] } }, include: ['src'] }; await fs.writeFile( tsconfigPath, JSON.stringify(tsconfigContent, null, 2), 'utf8' ); // Create component file const componentContent = `import React from 'react'; const ${capitalize(name)}: React.FC = () => { return (

Welcome to the ${name} theme!

You can edit this file at: ${componentFile}
); }; export const layout = { areaId: 'content', sortOrder: 10 }; export default ${capitalize(name)}; `; await fs.writeFile(componentFile, componentContent, 'utf8'); console.log(kleur.green(`Theme '${name}' created.`)); console.log(kleur.blue(`Edit your new page at: ${componentFile}`)); } catch (err: any) { console.error(kleur.red('Error creating theme:'), err); process.exit(1); } } createTheme().catch((err: any) => { if (err) { console.error(kleur.red('An unexpected error occurred:'), err); } process.exit(1); }); ================================================ FILE: packages/evershop/src/bin/theme/twizz.ts ================================================ #!/usr/bin/env node /* eslint-disable no-console */ import fs from 'fs/promises'; import path from 'path'; import boxen from 'boxen'; import enquirer from 'enquirer'; import kleur from 'kleur'; import { getConfig } from '../../lib/util/getConfig.js'; const { prompt } = enquirer; function parseRelativeImports(content: string): string[] { const relativeImports: string[] = []; const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)(?:\s*,\s*(?:\{[^}]*\}|\*\s+as\s+\w+|\w+))*\s+from\s+)?['"`]([^'"`]+)['"`]/g; let match; while ((match = importRegex.exec(content)) !== null) { const importPath = match[1]; // Check if it's a relative import (starts with ./ or ../) if (importPath.startsWith('./') || importPath.startsWith('../')) { relativeImports.push(importPath); } } return relativeImports; } function resolveImportPath( currentFilePath: string, importPath: string ): string { const currentDir = path.dirname(currentFilePath); const resolvedPath = path.resolve(currentDir, importPath); const extensions = ['.tsx', '.jsx', '.ts', '.js']; if (path.extname(resolvedPath)) { return resolvedPath; } for (const ext of extensions) { const pathWithExt = resolvedPath + ext; try { return pathWithExt; } catch { continue; } } for (const ext of extensions) { const indexPath = path.join(resolvedPath, `index${ext}`); try { return indexPath; } catch { continue; } } return resolvedPath; } // Utility: Recursively find all dependencies of a file async function findAllDependencies( filePath: string, visited: Set = new Set(), baseDir: string ): Promise { if (visited.has(filePath)) { return []; } visited.add(filePath); const dependencies: string[] = []; try { const content = await fs.readFile(filePath, 'utf8'); const relativeImports = parseRelativeImports(content); for (const importPath of relativeImports) { const resolvedPath = resolveImportPath(filePath, importPath); // Check if the resolved file exists and is within our base directory try { await fs.access(resolvedPath); // Only include files that are within our component structure if (resolvedPath.startsWith(baseDir)) { dependencies.push(resolvedPath); // Recursively find dependencies of this file const nestedDeps = await findAllDependencies( resolvedPath, visited, baseDir ); dependencies.push(...nestedDeps); } } catch { // File doesn't exist, try other extensions const extensions = ['.tsx', '.jsx', '.ts', '.js']; let found = false; for (const ext of extensions) { const pathWithExt = resolvedPath + ext; try { await fs.access(pathWithExt); if (pathWithExt.startsWith(baseDir)) { dependencies.push(pathWithExt); const nestedDeps = await findAllDependencies( pathWithExt, visited, baseDir ); dependencies.push(...nestedDeps); found = true; break; } } catch { continue; } } // Try index files if still not found if (!found) { for (const ext of extensions) { const indexPath = path.join(resolvedPath, `index${ext}`); try { await fs.access(indexPath); if (indexPath.startsWith(baseDir)) { dependencies.push(indexPath); const nestedDeps = await findAllDependencies( indexPath, visited, baseDir ); dependencies.push(...nestedDeps); break; } } catch { continue; } } } } } } catch (err) {} return [...new Set(dependencies)]; } async function scanDirectory(dir: string): Promise { let results: string[] = []; try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { results = results.concat(await scanDirectory(fullPath)); } else if ( entry.isFile() && (fullPath.endsWith('.jsx') || fullPath.endsWith('.tsx')) ) { results.push(fullPath); } } } catch (err) { // ignore errors if directory doesn't exist } return results; } async function scanModulesFrontStore(): Promise { const evershopDir = path.join( process.cwd(), 'node_modules', '@evershop', 'evershop' ); const modulesDir = (await isRealDirectory(evershopDir)) ? path.join(evershopDir, 'src', 'modules') : path.join(process.cwd(), 'packages', 'evershop', 'src', 'modules'); let results: string[] = []; try { const modules = await fs.readdir(modulesDir, { withFileTypes: true }); for (const mod of modules) { if (mod.isDirectory()) { const frontStoreDir = path.join( modulesDir, mod.name, 'pages', 'frontStore' ); const files = await scanDirectory(frontStoreDir); results = results.concat(files); } } } catch (err) {} return results; } async function isRealDirectory(path) { try { const stats = await fs.lstat(path); if (stats.isSymbolicLink()) { return false; } return stats.isDirectory(); } catch (err) { if (err.code === 'ENOENT') { return false; } throw err; } } async function getOverrideCandidates(): Promise { // Check if a folder @evershop/evershop exists in the node_modules const evershopDir = path.join( process.cwd(), 'node_modules', '@evershop', 'evershop' ); let commonDir, frontStoreDir; if (await isRealDirectory(evershopDir)) { commonDir = path.join(evershopDir, 'src', 'components', 'common'); frontStoreDir = path.join(evershopDir, 'src', 'components', 'frontStore'); } else { commonDir = path.join( process.cwd(), 'packages', 'evershop', 'src', 'components', 'common' ); frontStoreDir = path.join( process.cwd(), 'packages', 'evershop', 'src', 'components', 'frontStore' ); } const files1 = await scanDirectory(commonDir); const files2 = await scanDirectory(frontStoreDir); const files3 = await scanModulesFrontStore(); return [...files1, ...files2, ...files3]; } function getCurrentTheme(): string { const theme = getConfig('system.theme'); if (theme) { return theme; } else { console.error( kleur.red( 'No theme set in config/system.theme. Please set a theme before creating overrides.' ) ); process.exit(1); } } // Given an original file path and current theme, determine the destination override file path function getDestinationPath(originalPath: string, theme: string): string { const themeDir = path.join(process.cwd(), 'themes', theme, 'src'); const componentsIdx = originalPath.indexOf(path.join('src', 'components')); const modulesIdx = originalPath.indexOf(path.join('src', 'modules')); if (componentsIdx !== -1) { // For files under src/components, replicate structure under /components const relativePath = originalPath.substring( originalPath.indexOf('components') ); return path.join(themeDir, relativePath); } else if (modulesIdx !== -1) { // For files under src/modules/*/pages/frontStore/*, map to /pages/* const frontStoreMarker = path.join('pages', 'frontStore'); const markerIdx = originalPath.indexOf(frontStoreMarker); if (markerIdx !== -1) { const relativePath = originalPath.substring( markerIdx + frontStoreMarker.length ); // Ensure leading slash is removed const cleanedRelative = relativePath.replace( new RegExp(`^(\\${path.sep}|/)`), '' ); return path.join(themeDir, 'pages', cleanedRelative); } } // Fallback: put in theme root return path.join(themeDir, path.basename(originalPath)); } // Ensure directory exists async function ensureDir(dir: string): Promise { try { await fs.mkdir(dir, { recursive: true }); } catch (err) { // Ignore if exists } } async function createOverrideFile() { const candidates = await getOverrideCandidates(); if (candidates.length === 0) { console.error(kleur.red('No override candidates found.')); process.exit(1); } // Updated prompt: use 'autocomplete' and removed unsupported 'limit' property const relativeCandidates = candidates.map((filePath) => path.relative(process.cwd(), filePath) ); const response: any = await prompt({ type: 'autocomplete', name: 'file', message: 'Select a file to override:', initial: 0, choices: relativeCandidates }); const selectedRelative = response.file; const selectedFile = path.join(process.cwd(), selectedRelative); // Get current theme const theme = getCurrentTheme(); // Determine the base directory for dependency tracking const evershopDir = path.join( process.cwd(), 'node_modules', '@evershop', 'evershop' ); const baseDir = (await isRealDirectory(evershopDir)) ? path.join(evershopDir, 'src') : path.join(process.cwd(), 'packages', 'evershop', 'src'); // Find all dependencies of the selected file console.log(kleur.yellow('Analyzing dependencies...')); const dependencies = await findAllDependencies( selectedFile, new Set(), baseDir ); const allFiles = [selectedFile, ...dependencies]; console.log(kleur.cyan(`Found ${dependencies.length} dependencies:`)); dependencies.forEach((dep) => { console.log(kleur.gray(` ${path.relative(process.cwd(), dep)}`)); }); // Ask user if they want to copy dependencies if (dependencies.length > 0) { const confirmResponse: any = await prompt({ type: 'confirm', name: 'copyDependencies', message: `Copy ${dependencies.length} dependency files along with the main file?`, initial: true }); if (!confirmResponse.copyDependencies) { // Only copy the main file allFiles.splice(1); // Remove all dependencies, keep only the main file } } // Copy all files (main + dependencies if confirmed) const copiedFiles: string[] = []; for (const filePath of allFiles) { const destPath = getDestinationPath(filePath, theme); // Read content from file let content: string; try { content = await fs.readFile(filePath, 'utf8'); } catch (err) { console.error(kleur.red(`Error reading file ${filePath}:`), err); continue; } // Ensure destination directory exists const destDir = path.dirname(destPath); await ensureDir(destDir); // Write content to new file try { await fs.writeFile(destPath, content, 'utf8'); copiedFiles.push(destPath); } catch (err) { console.error(kleur.red(`Error writing file ${destPath}:`), err); } } // Display results if (copiedFiles.length > 0) { console.log( boxen( kleur.green( `Successfully created ${copiedFiles.length} override file(s):\n` ) + copiedFiles.map((file) => kleur.white(`• ${file}`)).join('\n'), { padding: 1, borderColor: 'green' } ) ); } else { console.error(kleur.red('No files were copied.')); process.exit(1); } } createOverrideFile().catch((err) => { console.log(err); if (!err) { console.log(kleur.yellow('Command cancelled.')); process.exit(0); } else { console.error(kleur.red('An unexpected error occurred:'), err); process.exit(1); } }); ================================================ FILE: packages/evershop/src/bin/user/changePassword.js ================================================ import 'dotenv/config'; import { select, update } from '@evershop/postgres-query-builder'; import yargs from 'yargs'; import { error, success } from '../../lib/log/logger.js'; import { pool } from '../../lib/postgres/connection.js'; import { hashPassword } from '../../lib/util/passwordHelper.js'; function isValidPassword(password) { return password.length >= 8; } const { argv } = yargs .option('email', { alias: 'e', description: 'User email', demandOption: true, type: 'string', validate: (email) => { if (email.length === 0) { throw new Error('Email is required'); } return true; } }) .option('password', { alias: 'p', description: 'New password', demandOption: true, type: 'string' }) .check((argv) => { if (!isValidPassword(argv.password)) { throw new Error( 'Invalid password. Password must be at least 8 characters long' ); } return true; }) .help(); async function updatePassword() { const { email, password } = argv; try { const user = await select() .from('admin_user') .where('email', '=', email) .load(pool); if (!user) { throw new Error('User not found'); } await update('admin_user') .given({ password: hashPassword(password) }) .where('admin_user_id', '=', user.admin_user_id) .execute(pool); success('Password is updated successfully'); process.exit(0); } catch (e) { error(e); process.exit(0); } } updatePassword(); ================================================ FILE: packages/evershop/src/bin/user/create.js ================================================ import 'dotenv/config'; import { insertOnUpdate } from '@evershop/postgres-query-builder'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { error, success } from '../../lib/log/logger.js'; import { pool } from '../../lib/postgres/connection.js'; import { hashPassword } from '../../lib/util/passwordHelper.js'; function isValidEmail(email) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); } function isValidPassword(password) { return password.length >= 8; } const { argv } = yargs(hideBin(process.argv)) .option('name', { alias: 'n', description: 'Admin user full name', demandOption: true, type: 'string', validate: (name) => { if (name.length === 0) { throw new Error('Full name is required'); } return true; } }) .option('email', { alias: 'e', description: 'User email', demandOption: true, type: 'string', validate: (email) => { if (!isValidEmail(email)) { throw new Error('Invalid email format'); } return true; } }) .option('password', { alias: 'p', description: 'User password', demandOption: true, type: 'string' }) .check((argv) => { if (!isValidPassword(argv.password)) { throw new Error( 'Invalid password. Password must be at least 8 characters long' ); } return true; }) .help(); async function createAdminUser() { const { name: full_name, email, password } = argv; // Insert the admin user try { await insertOnUpdate('admin_user', ['email']) .given({ full_name, email, password: hashPassword(password) }) .execute(pool); success('Admin user created successfully'); process.exit(0); } catch (e) { error(e); process.exit(0); } } createAdminUser(); ================================================ FILE: packages/evershop/src/components/admin/AttributeGroupSelector.tsx ================================================ import { SimplePagination } from '@components/common/SimplePagination.js'; import { Button } from '@components/common/ui/Button.js'; import { Input } from '@components/common/ui/Input.js'; import { Skeleton } from '@components/common/ui/Skeleton.js'; import { Check } from 'lucide-react'; import React from 'react'; import { useQuery } from 'urql'; import { AtLeastOne } from '../../types/atLeastOne.js'; const SearchQuery = ` query Query ($filters: [FilterInput!]) { attributeGroups(filters: $filters) { items { attributeGroupId uuid groupName } total } } `; interface AttributeGroupIdentifier { attributeGroupId?: string | number; uuid?: string; } const AttributeGroupListSkeleton: React.FC = () => { const skeletonItems = Array(5).fill(0); return (
{skeletonItems.map((_, index) => (
))}
); }; const isAttributeGroupSelected = ( attributeGroup: AttributeGroupIdentifier, selectedAttributeGroups: AtLeastOne[] ): boolean => { return selectedAttributeGroups.some( (selected) => (selected?.attributeGroupId && selected.attributeGroupId === attributeGroup.attributeGroupId) || (selected?.uuid && selected.uuid === attributeGroup.uuid) ); }; const AttributeGroupSelector: React.FC<{ onSelect: (id: string | number, uuid: string, name: string) => void; onUnSelect: (id: string | number, uuid: string, name: string) => void; selectedAttributeGroups: AtLeastOne[]; }> = ({ onSelect, onUnSelect, selectedAttributeGroups }) => { const [internalSelectedAttributeGroups, setInternalSelectedAttributeGroups] = React.useState[]>( selectedAttributeGroups || [] ); const [loading, setLoading] = React.useState(false); const limit = 10; const [inputValue, setInputValue] = React.useState(''); const [page, setPage] = React.useState(1); const [result, reexecuteQuery] = useQuery({ query: SearchQuery, variables: { filters: inputValue ? [ { key: 'name', operation: 'like', value: inputValue }, { key: 'page', operation: 'eq', value: page.toString() }, { key: 'limit', operation: 'eq', value: limit.toString() } ] : [ { key: 'limit', operation: 'eq', value: limit.toString() }, { key: 'page', operation: 'eq', value: page.toString() } ] }, pause: true }); React.useEffect(() => { reexecuteQuery({ requestPolicy: 'network-only' }); }, [page]); React.useEffect(() => { const timer = setTimeout(() => { setLoading(false); if (inputValue !== '') { reexecuteQuery({ requestPolicy: 'network-only' }); } }, 1500); return () => clearTimeout(timer); }, [inputValue]); const { data, fetching, error } = result as { data: { attributeGroups: { items: Array<{ attributeGroupId: string | number; uuid: string; groupName: string; }>; total: number; }; }; fetching: boolean; error: Error | undefined; }; if (error) { return (

There was an error fetching attribute groups. {error.message}

); } return (
) => { setInputValue(e.target.value); setLoading(true); }} />
{(fetching || loading) && } {!fetching && data && (
{data.attributeGroups.items.length === 0 && (
{inputValue ? (

No attribute groups found for query "{inputValue} ”

) : (

You have no attribute groups to display

)}
)} {data.attributeGroups.items.map((a) => (

{a.groupName}

{!isAttributeGroupSelected( a, internalSelectedAttributeGroups ) && ( )} {isAttributeGroupSelected( a, internalSelectedAttributeGroups ) && ( )}
))}
)}
); }; export { AttributeGroupSelector }; ================================================ FILE: packages/evershop/src/components/admin/CategorySelector.tsx ================================================ import { SimplePagination } from '@components/common/SimplePagination.js'; import { Button } from '@components/common/ui/Button.js'; import { Input } from '@components/common/ui/Input.js'; import { Skeleton } from '@components/common/ui/Skeleton.js'; import { Check } from 'lucide-react'; import React from 'react'; import { useQuery } from 'urql'; import { AtLeastOne } from '../../types/atLeastOne.js'; const SearchQuery = ` query Query ($filters: [FilterInput!]) { categories(filters: $filters) { items { categoryId uuid name path { name } } total } } `; interface CategoryIdentifier { categoryId?: string | number; uuid?: string; } const CategoryListSkeleton: React.FC = () => { const skeletonItems = Array(5).fill(0); return (
{skeletonItems.map((_, index) => (
))}
); }; const isCategorySelected = ( category: CategoryIdentifier, selectedCategories: AtLeastOne[] ): boolean => { return selectedCategories.some( (selected) => (selected?.categoryId && selected.categoryId === category.categoryId) || (selected?.uuid && selected.uuid === category.uuid) ); }; const CategorySelector: React.FC<{ onSelect: (id: string | number, uuid: string, name: string) => void; onUnSelect: (id: string | number, uuid: string, name: string) => void; selectedCategories: AtLeastOne[]; }> = ({ onSelect, onUnSelect, selectedCategories }) => { const [internalSelectedCategories, setInternalSelectedCategories] = React.useState[]>(selectedCategories || []); const [loading, setLoading] = React.useState(false); const limit = 10; const [inputValue, setInputValue] = React.useState(''); const [page, setPage] = React.useState(1); const [result, reexecuteQuery] = useQuery({ query: SearchQuery, variables: { filters: inputValue ? [ { key: 'name', operation: 'like', value: inputValue }, { key: 'page', operation: 'eq', value: page.toString() }, { key: 'limit', operation: 'eq', value: limit.toString() } ] : [ { key: 'limit', operation: 'eq', value: limit.toString() }, { key: 'page', operation: 'eq', value: page.toString() } ] }, pause: true }); React.useEffect(() => { reexecuteQuery({ requestPolicy: 'network-only' }); }, [page]); React.useEffect(() => { const timer = setTimeout(() => { setLoading(false); if (inputValue !== '') { reexecuteQuery({ requestPolicy: 'network-only' }); } }, 1500); return () => clearTimeout(timer); }, [inputValue]); const { data, fetching, error } = result as { data: { categories: { items: Array<{ categoryId: string | number; uuid: string; name: string; path: Array<{ name: string }>; }>; total: number; }; }; fetching: boolean; error: Error | undefined; }; if (error) { return (

There was an error fetching categories. {error.message}

); } return (
) => { setInputValue(e.target.value); setLoading(true); }} />
{(fetching || loading) && } {!fetching && data && (
{data.categories.items.length === 0 && (
{inputValue ? (

No categories found for query "{inputValue}”

) : (

You have no categories to display

)}
)} {data.categories.items.map((cat) => (

{cat.path.map((item, index) => ( {item.name} {index < cat.path.length - 1 && ' > '} ))}

{!isCategorySelected(cat, internalSelectedCategories) && ( )} {isCategorySelected(cat, internalSelectedCategories) && ( )}
))}
)}
); }; export { CategorySelector }; ================================================ FILE: packages/evershop/src/components/admin/CategoryTree.scss ================================================ .category-tree-container { background-color: #fff; z-index: 100; border-color: #c9cccf; padding: 10px; box-sizing: border-box; max-height: 250px; overflow-y: auto; overflow-x: hidden; } .skeleton-wrapper-category-tree { width: 100%; display: flex; justify-content: center; flex-direction: column; .skeleton:empty { width: 100%; height: 30px; border-radius: 3px; cursor: progress; background: linear-gradient(0.25turn, transparent, #f7f6f6, transparent), linear-gradient(#eee, #eee); background-repeat: no-repeat; animation: loading 1.5s infinite; } @keyframes loading { to { background-position: 315px 0, 0 0, 0 190px, 50px 195px; } } } ================================================ FILE: packages/evershop/src/components/admin/CategoryTree.tsx ================================================ import React from 'react'; import { useQuery } from 'urql'; import './CategoryTree.scss'; import RenderIfTrue from '@components/common/RenderIfTrue.jsx'; import { Folder, Minus, Plus } from 'lucide-react'; export interface CategoryTreeItem { categoryId: number; name: string; hasChildren: boolean; path: Array<{ name: string }>; children?: Array; } const categoriesQuery = ` query Query ($filters: [FilterInput]) { categories (filters: $filters) { items { categoryId, name hasChildren path { name } } } } `; const childrenQuery = ` query Query ($filters: [FilterInput]) { categories (filters: $filters) { items { categoryId, name path { name } hasChildren } } } `; const Skeleton = () => (
); export interface CategoryItemProps { category: CategoryTreeItem; selectedCategories?: CategoryTreeItem[]; onSelect: (category: CategoryTreeItem) => void; } function CategoryItem({ category, selectedCategories, onSelect }: CategoryItemProps) { const [expanded, setExpanded] = React.useState(false); const [result] = useQuery({ query: childrenQuery, variables: { filters: [{ key: 'parent', operation: 'eq', value: category.categoryId }] }, pause: !expanded }); const { data, fetching, error } = result; if (error) { return (
  • {error.message}
  • ); } const className = selectedCategories?.find( (item) => item.categoryId === category.categoryId ) ? 'flex justify-start gap-2 items-center p-2 rounded-md bg-green-100 transition-colors duration-500' : 'flex justify-start gap-2 items-center p-2 rounded-md hover:bg-gray-100 transition-colors duration-500'; return (
  • {data && data.categories.items.length > 0 && expanded && (
      {data.categories.items.map((child) => ( ))}
    )}
  • ); } CategoryItem.defaultProps = { category: {}, selectedCategory: {} }; interface CategoryTreeProps { selectedCategories?: CategoryTreeItem[]; onSelect: (category: CategoryTreeItem) => void; } function CategoryTree({ selectedCategories, onSelect }: CategoryTreeProps) { const [result] = useQuery({ query: categoriesQuery, variables: { filters: [{ key: 'parent', operation: 'eq', value: null }] } }); const { data, fetching, error } = result; if (fetching) { return ; } if (error) { return

    {error.message}

    ; } if (!data || !data.categories || data.categories.items.length === 0) { return
    There is no category
    ; } return (
      {data.categories.items.map((category) => ( ))}
    ); } CategoryTree.defaultProps = { selectedCategories: [] }; export { CategoryTree }; ================================================ FILE: packages/evershop/src/components/admin/CollectionSelector.tsx ================================================ import { SimplePagination } from '@components/common/SimplePagination.js'; import { Button } from '@components/common/ui/Button.js'; import { Input } from '@components/common/ui/Input.js'; import { Skeleton } from '@components/common/ui/Skeleton.js'; import { Check } from 'lucide-react'; import React from 'react'; import { useQuery } from 'urql'; import { AtLeastOne } from '../../types/atLeastOne.js'; const SearchQuery = ` query Query ($filters: [FilterInput!]) { collections(filters: $filters) { items { collectionId uuid name } total } } `; interface CollectionIdentifier { collectionId?: string | number; uuid?: string; } const CollectionListSkeleton: React.FC = () => { const skeletonItems = Array(5).fill(0); return (
    {skeletonItems.map((_, index) => (
    ))}
    ); }; const isCollectionSelected = ( collection: CollectionIdentifier, selectedCollections: AtLeastOne[] ): boolean => { return selectedCollections.some( (selected) => (selected?.collectionId && selected.collectionId === collection.collectionId) || (selected?.uuid && selected.uuid === collection.uuid) ); }; const CollectionSelector: React.FC<{ onSelect: (id: string | number, uuid: string, name: string) => void; onUnSelect: (id: string | number, uuid: string, name: string) => void; selectedCollections: AtLeastOne[]; }> = ({ onSelect, onUnSelect, selectedCollections }) => { const [internalSelectedCollections, setInternalSelectedCollections] = React.useState[]>( selectedCollections || [] ); const [loading, setLoading] = React.useState(false); const limit = 10; const [inputValue, setInputValue] = React.useState(''); const [page, setPage] = React.useState(1); const [result, reexecuteQuery] = useQuery({ query: SearchQuery, variables: { filters: inputValue ? [ { key: 'name', operation: 'like', value: inputValue }, { key: 'page', operation: 'eq', value: page.toString() }, { key: 'limit', operation: 'eq', value: limit.toString() } ] : [ { key: 'limit', operation: 'eq', value: limit.toString() }, { key: 'page', operation: 'eq', value: page.toString() } ] }, pause: true }); React.useEffect(() => { reexecuteQuery({ requestPolicy: 'network-only' }); }, [page]); React.useEffect(() => { const timer = setTimeout(() => { setLoading(false); if (inputValue !== '') { reexecuteQuery({ requestPolicy: 'network-only' }); } }, 1500); return () => clearTimeout(timer); }, [inputValue]); const { data, fetching, error } = result as { data: { collections: { items: Array<{ collectionId: string | number; uuid: string; name: string; }>; total: number; }; }; fetching: boolean; error: Error | undefined; }; if (error) { return (

    There was an error fetching collections. {error.message}

    ); } return (
    ) => { setInputValue(e.target.value); setLoading(true); }} />
    {(fetching || loading) && } {!fetching && data && (
    {data.collections.items.length === 0 && (
    {inputValue ? (

    No collections found for query "{inputValue}”

    ) : (

    You have no collections to display

    )}
    )} {data.collections.items.map((c) => (

    {c.name}

    {!isCollectionSelected(c, internalSelectedCollections) && ( )} {isCollectionSelected(c, internalSelectedCollections) && ( )}
    ))}
    )}
    ); }; export { CollectionSelector }; ================================================ FILE: packages/evershop/src/components/admin/FileBrowser.scss ================================================ /* FILE BROWSER */ .file-browser { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: #fff; z-index: 1000; padding: 20px; } .file-browser .loading { position: absolute; top: 50%; left: 50%; margin-top: -40px; margin-left: -40px; } .file-browser img { max-width: 100%; } .file-browser .image-item .inner { padding: 3px; box-sizing: border-box; border: 1px solid #e1e1e1; position: relative; } .file-browser .image-item .inner .select { position: absolute; bottom: 5px; right: 5px; font-size: 20px; } .image-tool__image-picture { margin-top: 0 !important; margin-bottom: 0 !important; } ================================================ FILE: packages/evershop/src/components/admin/FileBrowser.tsx ================================================ import { Button } from '@components/common/ui/Button.js'; import React from 'react'; import './FileBrowser.scss'; import { useQuery } from 'urql'; import Spinner from '@components/admin/Spinner.js'; import { Input } from '@components/common/ui/Input.js'; const GetApisQuery = ` query Query ($filters: [FilterInput!]) { browserApi: url(routeId: "fileBrowser", params: [{key: "0", value: ""}]) deleteApi: url(routeId: "fileDelete", params: [{key: "0", value: ""}]) uploadApi: url(routeId: "imageUpload", params: [{key: "0", value: ""}]) folderCreateApi: url(routeId: "folderCreate") } `; export interface File { isSelected?: boolean; name: string; url: string; } const File: React.FC<{ file: File; select: (url: File) => void; }> = ({ file, select }) => { const className = file.isSelected === true ? 'selected' : ''; return (
    { e.preventDefault(); select(file); }} > {file.isSelected === true && (
    )}
    ); }; const FileBrowser: React.FC<{ onInsert: (url: string) => void; isMultiple: boolean; close: () => void; }> = ({ onInsert, isMultiple, close }) => { const [error, setError] = React.useState(''); const [loading, setLoading] = React.useState(false); const [folders, setFolders] = React.useState([]); const [files, setFiles] = React.useState([]); const [currentPath, setCurrentPath] = React.useState< { name: string; index: number; }[] >([{ name: '', index: 0 }]); const newFolderRefInput = React.useRef(null); const browserApiRef = React.useRef(''); const deleteApiRef = React.useRef(''); const uploadApiRef = React.useRef(''); const folderCreateApiRef = React.useRef(''); const onSelectFolder = (e, f) => { e.preventDefault(); setCurrentPath( currentPath.concat({ name: f, index: currentPath.length + 1 }) ); }; const onSelectFolderFromBreadcrumb = (e, index) => { e.preventDefault(); const newPath = [] as { name: string; index: number }[]; currentPath.forEach((f) => { if (f.index <= index) newPath.push(f); }); setCurrentPath(newPath); }; const onSelectFile = (f) => { if (isMultiple === false) { setFiles( files.map((file) => { if (f.name === file.name) { file.isSelected = !file.isSelected; } else { file.isSelected = false; } return file; }) ); } else { setFiles( files.map((file) => { if (f.name === file.name) { file.isSelected = true; } else { file.isSelected = false; } return file; }) ); } }; const closeFileBrowser = (e) => { e.preventDefault(); close(); }; const createFolder = (e, folder) => { e.preventDefault(); if (!folder || !folder.trim()) { setError('Invalid folder name'); return; } const path = currentPath.map((f) => f.name); path.push(folder.trim()); setLoading(true); fetch(folderCreateApiRef.current, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: path.join('/') }), credentials: 'same-origin' }) .then((res) => res.json()) .then((response) => { if (!response.error) { // Get the first level folder, incase of recursive folder creation const recursiveFolders = folder.split('/'); setFolders([...new Set(folders.concat(recursiveFolders[0]))]); } else { setError(response.error.message); } }) .catch((err) => setError(err.message)) .finally(() => setLoading(false)); }; const deleteFile = () => { let file; files.forEach((f) => { if (f.isSelected === true) { file = f; } }); if (!file) { setError('No file selected'); } else { const path = currentPath.map((f) => f.name); path.push(file.name); setLoading(true); fetch(deleteApiRef.current + path.join('/'), { method: 'DELETE' }) .then((res) => res.json()) .then((response) => { if (!response.error) { setCurrentPath(currentPath.map((f) => f)); } else { setError(response.error.message); } }) .catch((err) => setError(err.message)) .finally(() => setLoading(false)); } }; const insertFile = () => { let file; files.forEach((f) => { if (f.isSelected === true) { file = f; } }); if (!file) { setError('No file selected'); } else { onInsert(file.url); } }; const onUpload = (e) => { e.persist(); const formData = new FormData(); for (let i = 0; i < e.target.files.length; i += 1) formData.append('images', e.target.files[i]); const path = [] as string[]; currentPath.forEach((f) => { path.push(f.name); }); setLoading(true); fetch(uploadApiRef.current + path.join('/'), { method: 'POST', body: formData }) .then((res) => res.json()) .then((response) => { if (!response.error) { setCurrentPath(currentPath.map((f) => f)); } else { setError(response.error.message); } }) .catch((err) => setError(err.message)) .finally(() => setLoading(false)); }; // Create a function to fetch files and folders to avoid code duplication const fetchFilesAndFolders = React.useCallback(() => { if (!browserApiRef.current) { return; } const path = currentPath.map((f) => f.name); setLoading(true); fetch(browserApiRef.current + path.join('/'), { method: 'GET' }) .then((res) => res.json()) .then((response) => { if (!response.error) { setFolders(response.data.folders); setFiles(response.data.files); } else { setError(response.error.message); } }) .catch((e) => setError(e.message)) .finally(() => setLoading(false)); }, [currentPath]); const [result] = useQuery({ query: GetApisQuery }); const { data, fetching, error: err } = result; if (data) { browserApiRef.current = data.browserApi; deleteApiRef.current = data.deleteApi; uploadApiRef.current = data.uploadApi; folderCreateApiRef.current = data.folderCreateApi; } // Fetch files and folders when APIs are ready React.useEffect(() => { if (data) { fetchFilesAndFolders(); } }, [currentPath, fetchFilesAndFolders, data]); if (err) { return (

    There was an error fetching file browser APIs. {err.message}

    ); } if (fetching) { return (
    ); } return (
    {loading === true && (
    )}
    {error}
    {files.length === 0 &&
    There is no file to display.
    }
    {files.map((f) => ( ))}
    ); }; export { FileBrowser }; ================================================ FILE: packages/evershop/src/components/admin/FormButtons.tsx ================================================ import { Button } from '@components/common/ui/Button.js'; import React from 'react'; import { useFormContext } from 'react-hook-form'; const FormButtons: React.FC<{ formId: string; cancelUrl: string; }> = ({ cancelUrl, formId }) => { const { formState: { isSubmitting } } = useFormContext(); return (
    ); }; export { FormButtons }; ================================================ FILE: packages/evershop/src/components/admin/ImageUploader.scss ================================================ .image-uploader-manager img { max-width: 100%; } .image-uploader-manager .image-list { grid-template-columns: repeat(4, 1fr); display: grid; grid-gap: 8px; grid-auto-rows: 1fr; } /* Single image mode - responsive to parent */ .image-uploader-manager .single-image-container { position: relative; width: 100%; max-width: 100%; margin: 0 auto; aspect-ratio: 1 / 1; /* Maintain square aspect ratio */ } .image-uploader-manager .single-image-container .image { width: 100%; height: 100%; max-width: 100%; position: absolute; top: 0; left: 0; } /* Position the remove icon above everything in single image mode */ .image-uploader-manager .single-image-container .image .remove { position: absolute; top: 10px; left: 10px; z-index: 3; /* Higher than the Upload overlay */ background-color: rgba(255, 255, 255, 0.7); /* Semi-transparent background */ border-radius: 50%; padding: 5px; } /* Additional styling for the remove button in single mode */ .single-mode-remove { box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); display: flex; align-items: center; justify-content: center; width: 30px; height: 30px; } .image-uploader-manager .single-image-container .uploader { position: absolute; top: 0; left: 0; width: 100%; height: 100%; max-width: 100%; background: transparent; border: none; opacity: 0; transition: opacity 0.2s ease-in-out; display: flex; justify-content: center; align-items: center; z-index: 2; } /* For small containers, adjust icon sizes */ @media (max-width: 400px) { .image-uploader-manager .single-image-container .uploader .uploader-icon label svg { width: 30px !important; height: 30px !important; } .single-mode-remove { width: 25px; height: 25px; } } .image-uploader-manager .single-image-container .uploader:hover { opacity: 1; background: rgba(0, 0, 0, 0.5); } .image-uploader-manager .single-image-container .uploader .uploader-icon label { color: white; font-size: 30px; } /* When there's no image yet in single mode */ .image-uploader-manager .single-image-container.no-image .uploader { opacity: 1; border-radius: var(--radius); background: #f5f5f5; border: 2px dashed var(--border); } .image-uploader-manager .single-image-container.no-image .uploader .uploader-icon label { color: var(--primary); } .image-uploader-manager .image-list .image { position: relative; padding: 5px; box-sizing: border-box; display: flex; justify-content: center; align-items: center; background: #fff; max-width: 150px; } .image-uploader-manager .image-list .uploader { position: relative; padding: 5px; box-sizing: border-box; display: flex; justify-content: center; align-items: center; border: 2px dashed var(--border); border-radius: var(--radius); background: #fff; max-width: 150px; } .image-uploader-manager .image-list .grid-item { display: flex; } /* Apply special styling for the first grid item */ .image-uploader-manager .image-list .grid-item:first-child, .image-uploader-manager .image-list .grid-item.first-item { grid-column: 1 / span 2; grid-row: 1 / span 2; max-width: 100%; } .image-uploader-manager .image-list .grid-item:first-child:after, .image-uploader-manager .image-list .grid-item.first-item:after { content: ''; display: block; padding-bottom: 100%; } .image-uploader-manager .image-list .image:first-child, .image-uploader-manager .image-list .image.first-item { grid-row: 1 / span 2; } .image-uploader-manager .image-list .image:first-child img, .image-uploader-manager .image-list .image.first-item img { max-width: 100%; } .image-uploader-manager .image-list { outline: 0; } .image-uploader-manager .image-list .image .img { width: 100%; display: flex; justify-content: center; align-items: center; } .image-uploader-manager .image-list .image .remove, .image-uploader-manager .image-list .image .zoom { position: absolute; top: 10px; } .image-uploader-manager .image-list .image .remove { left: 10px; } .image-uploader-manager .image-list .image .zoom { right: 10px; } .uploader .invisible { position: absolute; top: 0; left: 0; } .uploader .uploader-icon label { cursor: pointer; font-size: 18px; } ================================================ FILE: packages/evershop/src/components/admin/ImageUploader.tsx ================================================ import React from 'react'; import { toast } from 'react-toastify'; import uniqid from 'uniqid'; import { useQuery } from 'urql'; import { get } from '../../lib/util/get.js'; import './ImageUploader.scss'; import Spinner from '@components/admin/Spinner.js'; import { ImageUploaderSkeleton } from './ImageUploaderSkeleton.js'; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; export interface Image { uuid: string; url: string; path?: string; } const Upload: React.FC<{ imageUploadUrl: string; targetPath?: string; onUpload: (images: Image[]) => void | Promise; isSingleMode?: boolean; }> = ({ imageUploadUrl, targetPath, onUpload, isSingleMode }) => { const [uploading, setUploading] = React.useState(false); const onChange = (e) => { setUploading(true); e.persist(); const formData = new FormData(); for (let i = 0; i < e.target.files.length; i += 1) { formData.append('images', e.target.files[i]); } formData.append('targetPath', targetPath || ''); fetch(imageUploadUrl + (targetPath || ''), { method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then((response) => { const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { throw new TypeError('Something wrong. Please try again'); } return response.json(); }) .then(async (response) => { if (!response.error) { await onUpload( get(response, 'data.files', []).map((i) => ({ uuid: uniqid(), url: i.url, path: i.path })) ); } else { toast.error(get(response, 'error.message', 'Failed!')); } }) .catch((error) => { toast.error(error.message); }) .finally(() => { e.target.value = null; setUploading(false); }); }; const id = uniqid(); return (
    ); }; const Image: React.FC<{ image: Image; allowDelete?: boolean; onDelete: (image) => void | Promise; isFirst?: boolean; isSingleMode?: boolean; }> = ({ image, allowDelete, onDelete, isFirst, isSingleMode }) => { const [deleting, setDeleting] = React.useState(false); // Use ref to track if component is mounted const isMounted = React.useRef(true); // Set up effect for cleanup React.useEffect(() => { return () => { // When component unmounts, set ref to false isMounted.current = false; }; }, []); // Assign classes based on mode const classes = isSingleMode ? 'image border border-border rounded-lg' : `image border border-border rounded-lg grid-item ${ isFirst ? 'first-item' : '' }`; return (
    {allowDelete && ( { setDeleting(true); await onDelete(image); // Only update state if component is still mounted if (isMounted.current) { setDeleting(false); } }} onKeyDown={() => {}} > )} {deleting && (
    )}
    ); }; const SortableImage: React.FC<{ image: Image; allowDelete?: boolean; onDelete: (image) => void | Promise; isFirst?: boolean; }> = (props) => { const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: props.image.uuid }); const style = { transform: CSS.Transform.toString(transform), transition }; return (
    ); }; const GetUploadApiQuery = ` query Query ($filters: [FilterInput!]) { imageUploadUrl: url(routeId: "imageUpload", params: [{key: "0", value: ""}]) } `; export interface ImageUploaderProps { currentImages?: Array; isMultiple?: boolean; allowDelete?: boolean; onDelete?: (image: Image) => void | Promise; onUpload?: (images: Image[]) => void | Promise; targetPath?: string; allowSwap?: boolean; onSortEnd?: (oldIndex: number, newIndex: number) => void; } interface ImagesProps extends ImageUploaderProps { addImage: (imageArray: Image[]) => void; imageUploadUrl: string; onDelete: (image: Image) => void | Promise; onUpload: (images: Image[]) => void | Promise; targetPath?: string; onSortEnd?: (oldIndex: number, newIndex: number) => void; } const Images: React.FC = ({ allowDelete = true, currentImages, imageUploadUrl, onDelete, onUpload, targetPath, isMultiple, allowSwap, onSortEnd }) => { const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ); const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (active.id !== over?.id && onSortEnd && currentImages) { const oldIndex = currentImages.findIndex((img) => img.uuid === active.id); const newIndex = currentImages.findIndex((img) => img.uuid === over?.id); if (oldIndex !== -1 && newIndex !== -1) { onSortEnd(oldIndex, newIndex); } } }; if (!isMultiple) { const hasImage = currentImages && currentImages.length > 0; return (
    {hasImage ? ( ) : null}
    ); } else if (allowSwap && currentImages && currentImages.length > 1) { return ( img.uuid)}> {currentImages.map((image, index) => ( ))} ); } // Multi-image mode without drag and drop return ( <> {(currentImages || []).map((image, index) => ( ))} ); }; export function ImageUploader({ currentImages = [], isMultiple = true, allowDelete = true, onDelete, onUpload, allowSwap = true, targetPath, onSortEnd }: ImageUploaderProps) { const [images, setImages] = React.useState( currentImages.map((image) => ({ uuid: image.uuid, url: image.url, path: image.path })) ); const handleSortEnd = (oldIndex: number, newIndex: number) => { setImages((items) => { return arrayMove(items, oldIndex, newIndex); }); if (onSortEnd) { onSortEnd(oldIndex, newIndex); } }; const addImage = (imageArray: Image[]) => { if (!isMultiple) { // For single image mode, replace the current image setImages(imageArray); } else { setImages(images.concat(imageArray)); } }; const removeImage = (imageUuid) => { setImages(images.filter((i) => i.uuid !== imageUuid)); }; const onDeleteFn = async (image: Image) => { if (onDelete) { await onDelete(image); } removeImage(image.uuid); }; const onUploadFn = async (imageArray: Image[]) => { if (onUpload) { await onUpload(imageArray); } addImage(imageArray); }; const [result] = useQuery({ query: GetUploadApiQuery }); const { data, fetching, error } = result; if (error) { return (

    There was an error:{error.message}

    ); } else if (fetching) { return ; } else { return (
    ); } } ================================================ FILE: packages/evershop/src/components/admin/ImageUploaderSkeleton.tsx ================================================ import React from 'react'; interface ImageUploaderSkeletonProps { itemCount?: number; } export const ImageUploaderSkeleton: React.FC = ({ itemCount = 5 }) => { const items = Array(itemCount).fill(0); if (itemCount === 1) { return (
    ); } return (
    {items.slice(1, itemCount).map((_, index) => (
    ))}
    ); }; export default { ImageUploaderSkeleton }; ================================================ FILE: packages/evershop/src/components/admin/NavigationItem.scss ================================================ .nav-item { .menu-icon { padding-right: 10px; align-self: center; svg { width: 15px; height: 15px; } } } ================================================ FILE: packages/evershop/src/components/admin/NavigationItem.tsx ================================================ import React from 'react'; import './NavigationItem.scss'; export interface NavigationItemProps { Icon: React.ElementType; url: string; title: string; } export function NavigationItem({ Icon, url, title }: NavigationItemProps) { const [isActive, setIsActive] = React.useState(false); React.useEffect(() => { const checkActive = () => { const currentUrl = window.location.href; const currentUrlObj = new URL(currentUrl); const menuUrlObj = new URL(url); const currentPath = currentUrlObj.pathname; const menuPath = menuUrlObj.pathname; if (currentPath === menuPath) { setIsActive(true); return; } const menuSegments = menuPath.split('/').filter(Boolean); if (menuSegments.length >= 2 && currentPath.startsWith(menuPath + '/')) { const remainingPath = currentPath.substring(menuPath.length + 1); const nextSegment = remainingPath.split('/')[0]; const actionWords = ['new', 'create', 'add']; if (!actionWords.includes(nextSegment.toLowerCase())) { setIsActive(true); return; } } setIsActive(false); }; checkActive(); }, [url]); return (
  • {title}
  • ); } ================================================ FILE: packages/evershop/src/components/admin/NavigationItemGroup.scss ================================================ .nav-item { // Open, close transition &.closed { // Close transition // Hide the children .item-group { display: none; } } .item-group { .nav-item { > a { padding-left: 1.25rem; } } } } .root-label { span:first-child { padding-right: 10px; } } .root-nav-item { &:last-child { position: fixed; bottom: 0px; left: 0px; width: 200px; background-color: #fff; padding-bottom: 1.25rem; padding-top: 1rem; } } ================================================ FILE: packages/evershop/src/components/admin/NavigationItemGroup.tsx ================================================ import { NavigationItem, NavigationItemProps } from '@components/admin/NavigationItem.js'; import Area from '@components/common/Area.jsx'; import React from 'react'; import './NavigationItemGroup.scss'; interface NavigationItemGroupProps { id: string; name: string; items: NavigationItemProps[]; Icon: React.ElementType | null; url: string | null; } export function NavigationItemGroup({ id, name, items = [], Icon = null, url = null }: NavigationItemGroupProps) { return (
  • {Icon && ( )} {!url && {name}} {url && {name}}
      ({ component: { default: () => ( ) } }))} />
  • ); } NavigationItemGroup.defaultProps = { items: [], Icon: null, url: null }; ================================================ FILE: packages/evershop/src/components/admin/PageHeading.scss ================================================ .page-heading { margin: 1rem auto 2rem; h1 { font-size: 1.25rem; font-weight: 600; } } .breadcrum-icon { width: 2.125rem; height: 2.125rem; span { width: 100%; height: 100%; svg { fill: var(--icon); width: 65%; } } } ================================================ FILE: packages/evershop/src/components/admin/PageHeading.tsx ================================================ import Area from '@components/common/Area.js'; import React from 'react'; import './PageHeading.scss'; function BackIcon({ backUrl }: { backUrl?: string }) { if (!backUrl) return null; return ( ); } BackIcon.defaultProps = { backUrl: undefined }; function Heading({ heading }: { heading: string }) { return (

    {heading}

    ); } export interface PageHeadingProps { backUrl?: string; heading: string; } function PageHeading({ backUrl, heading }: PageHeadingProps) { if (!heading) { return null; } return (
    ); } PageHeading.defaultProps = { backUrl: undefined }; export { PageHeading }; ================================================ FILE: packages/evershop/src/components/admin/ProductListSkeleton.tsx ================================================ import { Skeleton } from '@components/common/ui/Skeleton.js'; import React from 'react'; export const ProductListSkeleton: React.FC = () => { const skeletonItems = Array(5).fill(0); return (
    {skeletonItems.map((_, index) => (
    ))}
    ); }; ================================================ FILE: packages/evershop/src/components/admin/ProductSelector.tsx ================================================ import { SimplePagination } from '@components/common/SimplePagination.js'; import { Button } from '@components/common/ui/Button.js'; import { Input } from '@components/common/ui/Input.js'; import { Check } from 'lucide-react'; import React from 'react'; import { toast } from 'react-toastify'; import { useQuery } from 'urql'; import { AtLeastOne } from '../../types/atLeastOne.js'; import { ProductListSkeleton } from './ProductListSkeleton.js'; const SearchQuery = ` query Query ($filters: [FilterInput!]) { products(filters: $filters) { items { productId uuid sku name price { regular { text } } image { url } } total } } `; type ProductIdentifier = { sku?: string; uuid?: string; productId?: string; }; const isProductSelected = ( product: ProductIdentifier, selectedProducts: Array> ): boolean => { return selectedProducts.some( (selected) => (selected?.sku && selected.sku === product.sku) || (selected?.uuid && selected.uuid === product.uuid) || (selected?.productId && selected.productId === product.productId) ); }; const ProductSelector: React.FC<{ onSelect: ( sku: string, uuid: string, productId: string ) => Promise | void; onUnSelect?: ( sku: string, uuid: string, productId: string ) => Promise | void; selectedProducts: Array>; }> = ({ onSelect, onUnSelect, selectedProducts }) => { const limit = 10; const [internalSelectedProducts, setSelectedProducts] = React.useState< Array> >(selectedProducts || []); const [inputValue, setInputValue] = React.useState(null); const [loading, setLoading] = React.useState(false); const [page, setPage] = React.useState(1); const [result, reexecuteQuery] = useQuery({ query: SearchQuery, variables: { filters: inputValue ? [ { key: 'keyword', operation: 'eq', value: inputValue }, { key: 'page', operation: 'eq', value: page.toString() }, { key: 'limit', operation: 'eq', value: limit.toString() } ] : [ { key: 'limit', operation: 'eq', value: limit.toString() }, { key: 'page', operation: 'eq', value: page.toString() } ] }, pause: true }); const selectProduct = async ( sku: string, uuid: string, productId: string ) => { setSelectedProducts((prev) => [...prev, { sku, uuid, productId }]); try { await onSelect(sku, uuid, productId); } catch (e) { toast.error(e.message); } }; const unSelectProduct = async ( sku: string, uuid: string, productId: string ) => { if (!onUnSelect) { return; } setSelectedProducts((prev) => prev.filter((product) => product?.sku !== sku) ); try { await onUnSelect(sku, uuid, productId); } catch (e) { toast.error(e.message); } }; React.useEffect(() => { reexecuteQuery({ requestPolicy: 'network-only' }); }, [page]); React.useEffect(() => { const timer = setTimeout(() => { setLoading(false); if (inputValue !== null) { reexecuteQuery({ requestPolicy: 'network-only' }); } }, 1500); return () => clearTimeout(timer); }, [inputValue]); const { data, fetching, error } = result; if (error) { return (

    There was an error fetching products. {error.message}

    ); } return (
    { setInputValue(e.target.value); setLoading(true); }} />
    {(fetching || loading) && } {!fetching && data && !loading && (
    {data.products.items.length === 0 && (
    {inputValue ? (

    No products found for query "{inputValue}”

    ) : (

    You have no products to display

    )}
    )} {data.products.items.map((product) => (
    {product.image?.url && ( {product.name} )} {!product.image?.url && ( )}

    {product.name}

    {product.sku}

    {!isProductSelected(product, internalSelectedProducts) && ( )} {isProductSelected(product, internalSelectedProducts) && ( )}
    ))}
    )}
    ); }; export { ProductSelector }; ================================================ FILE: packages/evershop/src/components/admin/SettingMenu.tsx ================================================ import Area from '@components/common/Area.js'; import React from 'react'; export function SettingMenu() { return (
    ); } ================================================ FILE: packages/evershop/src/components/admin/Spinner.jsx ================================================ import PropTypes from 'prop-types'; import React from 'react'; function Spinner({ width, height }) { return ( ); } Spinner.propTypes = { width: PropTypes.number, height: PropTypes.number }; Spinner.defaultProps = { width: 60, height: 60 }; export default Spinner; ================================================ FILE: packages/evershop/src/components/admin/Status.tsx ================================================ import { Badge } from '@components/common/ui/Badge.js'; import { TableCell } from '@components/common/ui/Table.js'; import React from 'react'; export interface StatusProps { status: number; } export function Status({ status }: StatusProps) { return (
    {status === 0 && Inactive} {status === 1 && Active}
    ); } ================================================ FILE: packages/evershop/src/components/admin/grid/GridPagination.tsx ================================================ import { Button } from '@components/common/ui/Button.js'; import { ButtonGroup } from '@components/common/ui/ButtonGroup.js'; import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from '@components/common/ui/Pagination.js'; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from '@components/common/ui/Select.js'; import React from 'react'; export interface GridPaginationProps { total: number; limit: number; page: number; } export function GridPagination({ total, limit, page }: GridPaginationProps) { const limitInput = React.useRef(null); React.useEffect(() => { if (limitInput.current) { limitInput.current.value = limit.toString(); } }, []); const onKeyPress = (e: React.ChangeEvent) => { e.preventDefault(); let pageNumber = parseInt(e.target.value, 10); if (page < 1) pageNumber = 1; if (page > Math.ceil(total / limit)) pageNumber = Math.ceil(total / limit); const url = new URL(window.location.href); url.searchParams.set('page', pageNumber.toString()); window.location.href = url.href; }; const onPrev = (e: React.MouseEvent) => { e.preventDefault(); const prev = page - 1; if (page === 1) return; const url = new URL(window.location.href); url.searchParams.set('page', prev.toString()); window.location.href = url.href; }; const onNext = (e: React.MouseEvent) => { e.preventDefault(); const next = page + 1; if (page * limit >= total) return; const url = new URL(window.location.href); url.searchParams.set('page', next.toString()); window.location.href = url.href; }; const onFirst = (e: React.MouseEvent) => { e.preventDefault(); if (page === 1) return; const url = new URL(window.location.href); url.searchParams.delete('page'); window.location.href = url.href; }; const onLast = (e: React.MouseEvent) => { e.preventDefault(); if (page === Math.ceil(total / limit)) return; const url = new URL(window.location.href); url.searchParams.set('page', Math.ceil(total / limit).toString()); window.location.href = url.href; }; return (
    { onPrev(e); }} /> {page > 1 && ( { onFirst(e); }} > 1 )} {page >= 3 && ( )} {page} {page < Math.ceil(total / limit) - 1 && ( )} {page * limit < total && ( { onLast(e); }} > {Math.ceil(total / limit)} )}
    ); } ================================================ FILE: packages/evershop/src/components/admin/grid/Thumbnail.tsx ================================================ import { Image } from '@components/common/Image.js'; import { TableCell } from '@components/common/ui/Table.js'; import React from 'react'; export interface ThumbnailProps { src?: string; name?: string; } export function Thumbnail({ src, name }: ThumbnailProps) { return (
    {src && ( {name )} {!src && ( )}
    ); } ================================================ FILE: packages/evershop/src/components/admin/grid/header/Dummy.tsx ================================================ import { TableCell } from '@components/common/ui/Table.js'; import React from 'react'; export function DummyColumnHeader({ title }: { title: string }) { return (
    {title}
    ); } ================================================ FILE: packages/evershop/src/components/admin/grid/header/Sortable.tsx ================================================ import { TableCell } from '@components/common/ui/Table.js'; import React from 'react'; function Up() { return ( ); } function Down() { return ( ); } function None() { return ( ); } export interface SortableHeaderProps { title: string; name: string; currentFilters: Array<{ key: string; value: string }>; } export function SortableHeader({ title, name, currentFilters = [] }: SortableHeaderProps) { const [currentDirection] = React.useState(() => { const currentOrderBy = currentFilters.find((filter) => filter.key === 'ob'); if (!currentOrderBy || currentOrderBy.value !== name) { return null; } else { return ( currentFilters.find((filter) => filter.key === 'od')?.value || 'asc' ); } }); const onChange = () => { const url = new URL(window.location.href); url.searchParams.set('ob', name); // Get the current direction by checking the currentFilters const currentDirection = currentFilters.find( (filter) => filter.key === 'od' ); if (!currentDirection || currentDirection.value === 'asc') { url.searchParams.set('od', 'desc'); } else { url.searchParams.set('od', 'asc'); } window.location.href = url.toString(); }; return (
    {title}
    ); } ================================================ FILE: packages/evershop/src/components/common/Area.tsx ================================================ import { useAppState } from '@components/common/context/app.js'; import { generateComponentKey } from '@evershop/evershop/lib/util/keyGenerator'; import React, { useEffect, useState } from 'react'; import type { ElementType } from 'react'; interface Component { id?: string; sortOrder?: number; props?: Record; component: { default: React.ElementType | React.ReactNode; }; } type AreaID = string; type ComponentID = string; interface Components { [key: AreaID]: { [key: ComponentID]: Component; }; } interface AreaProps { className?: string; coreComponents?: Component[]; id: string; noOuter?: boolean; wrapper?: React.ReactNode | string; wrapperProps?: Record; components?: Components; [key: string]: unknown; } interface Widget extends Component { props: Record; type: string; areaId: string[]; } const DEBUG_KEY = 'evershop_area_debug'; let toggleButtonMounted = false; let debugStylesMounted = false; function injectDebugStyles() { if (process.env.NODE_ENV !== 'development') return; if (debugStylesMounted || typeof document === 'undefined') return; debugStylesMounted = true; const style = document.createElement('style'); style.id = 'evershop-debug-styles'; style.textContent = [ '.evershop-debug-area__badge { opacity: 0; transition: opacity 0.15s ease; }', '.evershop-debug-area:hover > .evershop-debug-area__badge { opacity: 1; }' ].join('\n'); document.head.appendChild(style); } function injectToggleButton() { if (process.env.NODE_ENV !== 'development') return; if (toggleButtonMounted || typeof document === 'undefined') return; toggleButtonMounted = true; const btn = document.createElement('button'); const update = () => { const active = localStorage.getItem(DEBUG_KEY) === '1'; btn.textContent = active ? 'Debug: ON' : 'Debug: OFF'; btn.style.background = active ? '#3b82f6' : '#6b7280'; }; Object.assign(btn.style, { position: 'fixed', bottom: '16px', right: '16px', zIndex: '99999', padding: '6px 12px', borderRadius: '6px', border: 'none', color: '#fff', fontFamily: 'monospace', fontSize: '12px', cursor: 'pointer', boxShadow: '0 2px 8px rgba(0,0,0,0.3)', transition: 'background 0.2s' }); btn.title = 'Toggle Area debug mode'; update(); btn.addEventListener('click', () => { const next = localStorage.getItem(DEBUG_KEY) === '1' ? '0' : '1'; localStorage.setItem(DEBUG_KEY, next); // Notify all tabs and same-page listeners window.dispatchEvent( new StorageEvent('storage', { key: DEBUG_KEY, newValue: next, storageArea: localStorage }) ); update(); }); document.body.appendChild(btn); } function useDebugMode(): boolean { const [debug, setDebug] = useState(() => { if (process.env.NODE_ENV !== 'development') return false; try { return localStorage.getItem(DEBUG_KEY) === '1'; } catch { return false; } }); useEffect(() => { if (process.env.NODE_ENV !== 'development') return; injectToggleButton(); injectDebugStyles(); const handler = (e: StorageEvent) => { if (e.key === DEBUG_KEY) { setDebug(e.newValue === '1'); } }; window.addEventListener('storage', handler); return () => window.removeEventListener('storage', handler); }, []); return debug; } const AREA_COLORS = [ '#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16', '#f97316', '#6366f1', '#db2777', '#14b8a6', '#22c55e', '#eab308', '#f43f5e' ]; // Stable color per area ID function areaColor(id: string | undefined): string { if (!id) return AREA_COLORS[0]; let hash = 0; for (let i = 0; i < id.length; i++) { hash = (hash << 5) - hash + id.charCodeAt(i); hash |= 0; } return AREA_COLORS[Math.abs(hash) % AREA_COLORS.length]; } function Area(props: AreaProps) { const context = useAppState(); const debug = useDebugMode(); const { id, coreComponents, wrapperProps, noOuter, wrapper, className, components } = props; const areaComponents = (() => { const areaCoreComponents = coreComponents || []; const widgets = context.widgets || []; const wildCardWidgets = components?.['*'] || {}; const assignedWidgets: Component[] = []; widgets.forEach((widget: Widget) => { const adminKey = generateComponentKey(`admin_widget_${widget.type}`); const frontKey = generateComponentKey(`widget_${widget.type}`); const w = wildCardWidgets[adminKey] || wildCardWidgets[frontKey]; if (widget.areaId.includes(id) && w !== undefined) { assignedWidgets.push({ id: widget.id, sortOrder: widget.sortOrder, props: widget.props, component: w.component }); } }); const cs = components?.[id] === undefined ? areaCoreComponents.concat(assignedWidgets) : areaCoreComponents .concat(Object.values(components[id])) .concat(assignedWidgets); return cs.sort( (obj1, obj2) => (obj1.sortOrder || 0) - (obj2.sortOrder || 0) ); })(); const { propsMap } = context; // In debug mode, always use a real wrapper element so borders/badges can render. // noOuter is intentionally ignored when debug is active. // The process.env.NODE_ENV guard lets Terser statically eliminate this in production. const effectiveNoOuter = process.env.NODE_ENV === 'development' && debug ? false : noOuter; let WrapperComponent: ElementType = React.Fragment; if (effectiveNoOuter !== true) { if (wrapper !== undefined) { WrapperComponent = wrapper as ElementType; } else { WrapperComponent = 'div'; } } let areaWrapperProps: Record = {}; if (effectiveNoOuter === true) { areaWrapperProps = {}; } else if (typeof wrapperProps === 'object' && wrapperProps !== null) { areaWrapperProps = { className: className || '', ...wrapperProps }; } else { areaWrapperProps = { className: className || '' }; } const color = process.env.NODE_ENV === 'development' && debug ? areaColor(id) : ''; if ( process.env.NODE_ENV === 'development' && debug && effectiveNoOuter !== true ) { const existingStyle = areaWrapperProps.style || {}; const existingClass = (areaWrapperProps.className || '') as string; areaWrapperProps = { ...areaWrapperProps, className: `${existingClass} evershop-debug-area`.trim(), style: { ...existingStyle, position: 'relative', border: `2px dashed ${color}`, padding: '5px', boxSizing: 'border-box', minHeight: '32px' } }; } const renderedChildren = areaComponents.map((w, index) => { const C = w.component.default; const { id: componentId } = w; const propsData = context.graphqlResponse; const propKeys = componentId !== undefined ? propsMap[componentId] || [] : []; const componentProps = propKeys.reduce( (acc: Record, map: Record) => { const { origin, alias } = map; acc[origin] = propsData[alias]; return acc; }, {} ); if (w.props) { Object.assign(componentProps, w.props); } let rendered: React.ReactNode = null; if (React.isValidElement(C)) { rendered = {C}; } else if (typeof C === 'string') { rendered = ; } else if (typeof C === 'function') { rendered = ; } if (!debug || rendered === null || process.env.NODE_ENV !== 'development') { return rendered; } return (
    order: {w.sortOrder ?? 0} {rendered}
    ); }); if (process.env.NODE_ENV === 'development' && debug) { return ( #{id} {renderedChildren} ); } return ( {renderedChildren} ); } Area.defaultProps = { className: undefined, coreComponents: [], noOuter: false, wrapper: 'div', wrapperProps: {} }; export { Area }; export default Area; ================================================ FILE: packages/evershop/src/components/common/Editor.scss ================================================ .prose-base { margin-top: 1rem; margin-bottom: 1rem; } ================================================ FILE: packages/evershop/src/components/common/Editor.tsx ================================================ import { getColumnClasses } from '@components/common/form/editor/GetColumnClasses.js'; import { getRowClasses } from '@components/common/form/editor/GetRowClasses.js'; import { Row } from '@components/common/form/Editor.js'; import { Image as ResponsiveImage } from '@components/common/Image.js'; import React from 'react'; import './Editor.scss'; const Paragraph: React.FC<{ data: { text: string } }> = ({ data }) => { return

    ; }; const Header: React.FC<{ data: { level: number; text: string } }> = ({ data }) => { const tagName = `h${data.level}` as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; return React.createElement(tagName, null, data.text); }; const List: React.FC<{ data: { items: string[] } }> = ({ data }) => { return (

      {data.items.map((item, index) => (
    • {item}
    • ))}
    ); }; const Quote: React.FC<{ data: { text: string; caption?: string } }> = ({ data }) => { return (

    "{data.text}"

    {data.caption && - {data.caption}}
    ); }; const Image: React.FC<{ data: { file: { url: string; width?: number; height?: number }; caption?: string; withBorder?: boolean; withBackground?: boolean; stretched?: boolean; link?: string; }; columnSize: number; }> = ({ data, columnSize }) => { const { file, caption, withBorder, withBackground, stretched, link } = data; const imageStyles = { border: withBorder ? '1px solid #ccc' : 'none', backgroundColor: withBackground ? '#f9f9f9' : 'transparent', width: stretched ? '100%' : 'auto', display: 'block', maxWidth: '100%', margin: '0 auto' }; const imageWidth = file.width || 800; const imageHeight = file.height || (file.width ? Math.round(file.width * 0.75) : 600); // Calculate responsive sizes based on the columnSize prop // columnSize represents the fraction of the row that this column occupies (e.g., 1/2, 1/3, 2/3, etc.) let sizesValue: string; sizesValue = '100vw'; // On mobile, always full viewport width if (columnSize <= 0.25) { sizesValue = '(max-width: 640px) 100vw, (max-width: 768px) 80vw, 25vw'; } else if (columnSize <= 0.34) { sizesValue = '(max-width: 640px) 100vw, (max-width: 768px) 80vw, 33vw'; } else if (columnSize <= 0.5) { sizesValue = '(max-width: 640px) 100vw, (max-width: 768px) 80vw, 50vw'; } else if (columnSize <= 0.67) { sizesValue = '(max-width: 640px) 100vw, (max-width: 768px) 80vw, 67vw'; } else if (columnSize <= 0.75) { sizesValue = '(max-width: 640px) 100vw, (max-width: 768px) 80vw, 75vw'; } else { sizesValue = '(max-width: 640px) 100vw, 100vw'; } const responsiveSizes = sizesValue; const imageElement = ( ); return (
    {link ? ( {imageElement} ) : ( imageElement )} {caption && (

    {caption}

    )}
    ); }; const RawHtml: React.FC<{ data: { html: string } }> = ({ data }) => { return
    ; }; const RenderEditorJS: React.FC<{ blocks: Array<{ type: string; data: any }>; columnSize: number; // Renamed from 'size' to 'columnSize' for clarity }> = ({ blocks, columnSize }) => { return (
    {blocks.map((block, index) => { switch (block.type) { case 'paragraph': return ; case 'header': return
    ; case 'list': return ; case 'image': return ( ); case 'quote': return ; case 'raw': return ; default: return null; } })}
    ); }; interface EditorProps { rows: Row[]; } export function Editor({ rows }: EditorProps) { return (
    {rows.map((row, index) => { const rowClasses = getRowClasses(row.size); return (
    {row.columns.map((column, index) => { const columnClasses = getColumnClasses(column.size); return (
    {column.data?.blocks && ( )}
    ); })}
    ); })}
    ); } ================================================ FILE: packages/evershop/src/components/common/ExtendableTable.tsx ================================================ import Area from '@components/common/Area.js'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@components/common/ui/Table.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; export interface TableColumn { key: string; header: { label: React.ReactNode; className?: string; }; sortable?: boolean; width?: string; className?: string; isRemoved?: boolean; render?: (row: T, rowIndex?: number, loading?: boolean) => React.ReactNode; } export interface TableContextValue { columns: TableColumn[]; setColumns: React.Dispatch[]>>; tableData: T[]; currentSort?: { key: string; direction: 'asc' | 'desc' }; addColumnBefore: (newColumn: TableColumn, beforeColumnKey: string) => void; addColumnAfter: (newColumn: TableColumn, afterColumnKey: string) => void; removeColumn: (key: string) => void; tableName: string; } interface TableProviderProps { children: React.ReactNode; name: string; initialColumns: TableColumn[]; tableData: T[]; onSort?: (key: string, direction: 'asc' | 'desc') => void; currentSort?: { key: string; direction: 'asc' | 'desc' }; } const TableContext = React.createContext(null); export function useTableContext(): TableContextValue { const context = React.useContext(TableContext); if (!context) { throw new Error('useTableContext must be used within a TableProvider'); } return context as TableContextValue; } export function TableProvider({ children, name, initialColumns, tableData, onSort, currentSort }: TableProviderProps) { const [columns, setColumns] = React.useState[]>(initialColumns); // Update columns when props change React.useEffect(() => { setColumns(initialColumns.map((col) => ({ ...col }))); }, [initialColumns]); const addColumnBefore = React.useCallback( (newColumn: TableColumn, beforeColumnKey: string) => { setColumns((cols) => { // Find index of the column to insert before const index = cols.findIndex((col) => col.key === beforeColumnKey); // If found, insert before it (index), else add to the start const position = index !== -1 ? index : 0; return [...cols.slice(0, position), newColumn, ...cols.slice(position)]; }); }, [] ); const addColumnAfter = React.useCallback( (newColumn: TableColumn, afterColumnKey: string) => { setColumns((cols) => { // Find index of the column to insert after const index = cols.findIndex((col) => col.key === afterColumnKey); // If found, insert after it (index + 1), else add to the end const position = index !== -1 ? index + 1 : cols.length; return [...cols.slice(0, position), newColumn, ...cols.slice(position)]; }); }, [] ); const removeColumn = React.useCallback((key: string) => { setColumns((cols) => cols.map((col) => (col.key === key ? { ...col, isRemoved: true } : col)) ); }, []); const contextValue: TableContextValue = { columns, setColumns, tableData, currentSort, addColumnBefore, addColumnAfter, removeColumn, tableName: name }; return ( {children} ); } interface ExtendableTableProps { name: string; columns: TableColumn[]; initialData: T[]; loading?: boolean; noHeader?: boolean; emptyMessage?: string; onSort?: (key: string, direction: 'asc' | 'desc') => void; currentSort?: { key: string; direction: 'asc' | 'desc' }; className?: string; } export function ExtendableTable({ name, columns, initialData, loading = false, noHeader = false, emptyMessage = _('No data available'), onSort, currentSort, className = '' }: ExtendableTableProps) { const handleSort = (key: string) => { if (!onSort) return; const direction = currentSort?.key === key && currentSort.direction === 'asc' ? 'desc' : 'asc'; onSort(key, direction); }; return ( ); } // Separate component to use the context function TableContent({ loading = false, noHeader = false, onSort, currentSort, emptyMessage, className }: { loading?: boolean; noHeader?: boolean; onSort?: (key: string, direction: 'asc' | 'desc') => void; currentSort?: { key: string; direction: 'asc' | 'desc' }; emptyMessage: string; className: string; }) { const { columns, tableData } = useTableContext(); const handleSort = (key: string) => { if (!onSort) return; const direction = currentSort?.key === key && currentSort.direction === 'asc' ? 'desc' : 'asc'; onSort(key, direction); }; return ( <> {!noHeader && ( {columns .filter((col) => !col.isRemoved) .map((col) => ( col.sortable && handleSort(col.key)} style={{ width: col.width }} > {col.header.label} {col.sortable && currentSort?.key === col.key && ( {currentSort.direction === 'asc' ? '↑' : '↓'} )} ))} )} {tableData.length === 0 ? ( !col.isRemoved).length} > {emptyMessage} ) : ( tableData.map((row, rowIndex) => ( {columns .filter((col) => !col.isRemoved) .map((col) => ( {col.render ? col.render(row, rowIndex, loading) : row[col.key]} ))} )) )}
    ); } ================================================ FILE: packages/evershop/src/components/common/Image.tsx ================================================ import { parseImageSizes } from '@evershop/evershop/lib/util/parseImageSizes'; import React from 'react'; export type ImageProps = { src: string; width: number; // Intrinsic width of the image height: number; // Intrinsic height of the image alt: string; quality?: number; priority?: boolean; sizes?: string; loading?: 'eager' | 'lazy' | undefined; decoding?: 'async' | 'auto' | 'sync' | undefined; objectFit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down' | 'unset'; style?: React.CSSProperties; } & React.ImgHTMLAttributes; export function Image({ src, width, height, alt, quality = 75, loading = 'eager', decoding = 'async', priority = false, sizes = '100vw', objectFit = 'unset', ...props }: ImageProps): React.ReactElement | null { const generateSrcSet = (): string => { const imageSizes = parseImageSizes(sizes); // Don't upscale beyond 3 times the original width, but be smarter about filtering let filteredSizes = imageSizes.filter((size) => size <= width * 3); if (filteredSizes.length < 2) { // Add the original width filteredSizes.push(width); const smallerSizes = [ Math.round(width * 0.5), // 50% of original Math.round(width * 0.75) // 75% of original ].filter((size) => size >= 200 && !filteredSizes.includes(size)); // Don't go too small filteredSizes = [...filteredSizes, ...smallerSizes]; } if (!filteredSizes.includes(width)) { filteredSizes.push(width); } filteredSizes = [...new Set(filteredSizes)].sort((a, b) => a - b); return filteredSizes .map((size) => { // Construct the URL pointing to our image API const url = `/images?src=${encodeURIComponent( src )}&w=${size}&q=${quality}`; return `${url} ${size}w`; }) .join(', '); }; const srcset = generateSrcSet(); const fallbackSrc = `/images?src=${encodeURIComponent( src )}&w=${width}&q=${quality}`; // Prepare the base style with responsive behavior const baseStyle = { // Modern responsive image approach maxWidth: '100%', // Ensure image doesn't exceed its container height: 'auto', // Maintain aspect ratio objectFit: objectFit, aspectRatio: `${width} / ${height}` // Maintain aspect ratio }; return ( {alt} ); } ================================================ FILE: packages/evershop/src/components/common/Link.tsx ================================================ /* eslint-disable no-console */ import React from 'react'; /** * Valid values for the crossorigin attribute based on MDN specification */ export type CrossOrigin = 'anonymous' | 'use-credentials'; /** * Valid values for the fetchpriority attribute */ export type FetchPriority = 'high' | 'low' | 'auto'; /** * Valid values for the referrerpolicy attribute */ export type ReferrerPolicy = | 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url'; /** * Valid values for the as attribute when used with preload/modulepreload */ export type AsType = | 'audio' | 'document' | 'embed' | 'fetch' | 'font' | 'image' | 'object' | 'script' | 'style' | 'track' | 'video' | 'worker'; /** * Valid values for the blocking attribute */ export type BlockingType = 'render'; /** * Complete props interface for the Link component */ export interface LinkProps extends Omit< React.LinkHTMLAttributes, 'as' | 'crossOrigin' | 'fetchPriority' | 'referrerPolicy' > { href: string; rel: string; as?: AsType; blocking?: BlockingType; crossOrigin?: CrossOrigin; disabled?: boolean; fetchPriority?: FetchPriority; hrefLang?: string; imageSizes?: string; imageSrcSet?: string; integrity?: string; media?: string; referrerPolicy?: ReferrerPolicy; sizes?: string; title?: string; type?: string; } /** * Validates that required props are present based on rel value */ const validateProps = (props: LinkProps): void => { if (process.env.NODE_ENV === 'development') { const { rel, as: asType, crossOrigin, href } = props; // Check for 'as' attribute requirement with preload if (rel === 'preload' && !asType) { console.warn('Link: The "as" attribute is required when rel="preload"'); } // Check for crossOrigin requirement with certain as values if (asType && ['fetch', 'font'].includes(asType) && !crossOrigin) { console.warn( `Link: The "crossOrigin" attribute is required when as="${asType}"` ); } // Validate href is a proper URL format if (href && !href.match(/^(https?:\/\/|\/|\.\/|\.\.\/|\w+:)/)) { console.warn( `Link: Invalid href format "${href}". Expected a valid URL or path.` ); } } }; /** * Link component that renders an HTML element with comprehensive HTML5 support. * * This component supports all standard HTML5 link element attributes including: * - Resource linking (stylesheets, icons, etc.) * - Preloading resources with rel="preload" * - Module preloading with rel="modulepreload" * - CORS handling with crossOrigin * - Security features like Subresource Integrity * - Performance hints with fetchPriority * - Media queries for conditional loading * - All modern web standards compliance * * @example * // Basic stylesheet * * * @example * // Preload font with CORS * * * @example * // Responsive stylesheet with media query * * * @example * // Icon with sizes * */ export function Link(props: LinkProps): React.ReactElement { const { href, rel, as: asType, blocking, crossOrigin, disabled, fetchPriority, hrefLang, imageSizes, imageSrcSet, integrity, media, referrerPolicy, sizes, title, type, ...otherProps } = props; // Validate props in development validateProps(props); // Build props object with only defined attributes const linkProps: Record = { href, rel, ...otherProps }; // Add optional attributes only if they are defined if (asType !== undefined) linkProps.as = asType; if (blocking !== undefined) linkProps.blocking = blocking; if (crossOrigin !== undefined) linkProps.crossOrigin = crossOrigin; if (disabled !== undefined) linkProps.disabled = disabled; if (fetchPriority !== undefined) linkProps.fetchPriority = fetchPriority; if (hrefLang !== undefined) linkProps.hrefLang = hrefLang; if (imageSizes !== undefined) linkProps.imageSizes = imageSizes; if (imageSrcSet !== undefined) linkProps.imageSrcSet = imageSrcSet; if (integrity !== undefined) linkProps.integrity = integrity; if (media !== undefined) linkProps.media = media; if (referrerPolicy !== undefined) linkProps.referrerPolicy = referrerPolicy; if (sizes !== undefined) linkProps.sizes = sizes; if (title !== undefined) linkProps.title = title; if (type !== undefined) linkProps.type = type; return ; } /** * Convenience component for stylesheet links */ export function Stylesheet({ href, media, title, disabled, integrity, crossOrigin, fetchPriority, ...props }: Omit & { media?: string; title?: string; disabled?: boolean; }): React.ReactElement { return ( ); } /** * Convenience component for favicon links */ export function Favicon({ href, sizes, type = 'image/x-icon', ...props }: Omit & { sizes?: string; type?: string; }): React.ReactElement { return ; } /** * Convenience component for Apple Touch Icon links */ export function AppleTouchIcon({ href, sizes, type = 'image/png', ...props }: Omit & { sizes?: string; type?: string; }): React.ReactElement { return ( ); } /** * Convenience component for resource preloading */ export function Preload({ href, as, type, crossOrigin, integrity, fetchPriority, media, imageSizes, imageSrcSet, ...props }: Omit & { as: AsType; type?: string; crossOrigin?: CrossOrigin; integrity?: string; fetchPriority?: FetchPriority; media?: string; imageSizes?: string; imageSrcSet?: string; }): React.ReactElement { return ( ); } /** * Convenience component for module preloading */ export function ModulePreload({ href, as, integrity, fetchPriority, crossOrigin, ...props }: Omit & { as?: AsType; integrity?: string; fetchPriority?: FetchPriority; crossOrigin?: CrossOrigin; }): React.ReactElement { return ( ); } /** * Convenience component for DNS prefetch */ export function DNSPrefetch({ href, ...props }: Omit): React.ReactElement { return ; } /** * Convenience component for preconnect */ export function Preconnect({ href, crossOrigin, ...props }: Omit & { crossOrigin?: CrossOrigin }): React.ReactElement { return ( ); } ================================================ FILE: packages/evershop/src/components/common/LoadingBar.scss ================================================ .loading-bar { background: #058c8c; height: 4px; display: block; position: fixed; top: 0; left: 0; -webkit-transition: width 1.5s; -moz-transition: width 1.5s; -o-transition: width 1.5s; transition: width 1.5s; width: 0%; z-index: 1001; } ================================================ FILE: packages/evershop/src/components/common/LoadingBar.tsx ================================================ import { useAppState } from '@components/common/context/app.js'; import React from 'react'; import './LoadingBar.scss'; const LoadingBar = function LoadingBar() { const { fetching } = useAppState(); const [width, setWidth] = React.useState(0); const widthRef = React.useRef(0); React.useEffect(() => { widthRef.current = width; if (fetching === true) { // Random number between 1 and 3 const step = Math.random() * (3 - 1) + 1; // Random number between 85 and 95 const peak = Math.random() * (95 - 85) + 85; if (widthRef.current < peak) { const timer = setTimeout(() => setWidth(widthRef.current + step), 0); return () => clearTimeout(timer); } } else if (widthRef.current === 100) { setWidth(0); widthRef.current = 0; } else if (widthRef.current !== 0) { setWidth(100); } }); return (
    ); }; export { LoadingBar }; ================================================ FILE: packages/evershop/src/components/common/Meta.tsx ================================================ /* eslint-disable no-console */ import React from 'react'; interface BaseMetaProps { charset?: string; content?: string; httpEquiv?: | 'content-type' | 'default-style' | 'refresh' | 'x-ua-compatible' | 'content-security-policy'; lang?: string; scheme?: string; media?: string; } interface NameMetaProps extends BaseMetaProps { name: | 'description' | 'keywords' | 'author' | 'viewport' | 'robots' | 'generator' | 'theme-color' | 'application-name' | 'color-scheme' | 'referrer' | string; property?: never; itemProp?: never; } interface PropertyMetaProps extends BaseMetaProps { property: string; name?: never; itemProp?: never; } interface ItemPropMetaProps extends BaseMetaProps { itemProp: string; itemType?: string; itemId?: string; name?: never; property?: never; httpEquiv?: never; } interface HttpEquivMetaProps extends BaseMetaProps { httpEquiv: | 'content-type' | 'default-style' | 'refresh' | 'x-ua-compatible' | 'content-security-policy'; content: string; name?: never; property?: never; itemProp?: never; } interface CharsetOnlyProps { charset: 'utf-8' | string; content?: never; name?: never; property?: never; itemProp?: never; httpEquiv?: never; lang?: never; scheme?: never; media?: never; } type MetaProps = | NameMetaProps | PropertyMetaProps | ItemPropMetaProps | HttpEquivMetaProps | CharsetOnlyProps; const VALID_HTTP_EQUIV = [ 'content-type', 'default-style', 'refresh', 'x-ua-compatible', 'content-security-policy' ] as const; const REQUIRED_CONTENT_ATTRIBUTES = [ 'name', 'property', 'itemProp', 'httpEquiv' ] as const; function validateMetaProps(props: any): { isValid: boolean; errors: string[] } { const errors: string[] = []; const hasIdentifier = [ 'name', 'property', 'itemProp', 'httpEquiv', 'charset' ].some((attr) => props[attr] !== undefined); if (!hasIdentifier) { errors.push( 'Meta tag must have at least one identifier attribute (name, property, itemProp, httpEquiv, or charset)' ); } if (props.charset && props.charset.toLowerCase() !== 'utf-8') { errors.push('charset attribute must be "utf-8" for HTML5 documents'); } if (props.itemProp && (props.name || props.httpEquiv || props.charset)) { errors.push( 'itemProp attribute cannot be used with name, http-equiv, or charset attributes' ); } const needsContent = REQUIRED_CONTENT_ATTRIBUTES.some( (attr) => props[attr] !== undefined ); if (needsContent && !props.content) { errors.push( 'Meta tag with name, property, itemProp, or httpEquiv must have content attribute' ); } if (props.media && props.name !== 'theme-color') { errors.push('media attribute is only valid when name="theme-color"'); } if (props.httpEquiv && !VALID_HTTP_EQUIV.includes(props.httpEquiv)) { errors.push( `Invalid httpEquiv value: ${ props.httpEquiv }. Valid values: ${VALID_HTTP_EQUIV.join(', ')}` ); } const identifierCount = ['name', 'property', 'itemProp'].filter( (attr) => props[attr] !== undefined ).length; if (identifierCount > 1) { errors.push( 'Meta tag cannot have multiple identifier attributes (name, property, itemProp)' ); } if (props.itemProp) { if (props.itemType && !props.itemType.startsWith('http')) { errors.push('itemType should be a valid URL (typically schema.org URL)'); } } return { isValid: errors.length === 0, errors }; } function sanitizeMetaProps(props: any): Record { const allowedAttributes = [ 'charset', 'name', 'content', 'httpEquiv', 'property', 'itemProp', 'itemType', 'itemId', 'lang', 'scheme', 'media' ]; return Object.keys(props) .filter( (key) => allowedAttributes.includes(key) && props[key] !== undefined && props[key] !== null ) .reduce((obj, key) => { obj[key] = String(props[key]).trim(); return obj; }, {} as Record); } export function Meta(props: MetaProps) { if (process.env.NODE_ENV === 'development') { const validation = validateMetaProps(props); if (!validation.isValid) { console.error('Meta component validation errors:', validation.errors); validation.errors.forEach((error) => console.error(`Meta: ${error}`)); } } const sanitizedProps = sanitizeMetaProps(props); if (Object.keys(sanitizedProps).length === 0) { if (process.env.NODE_ENV === 'development') { console.warn('Meta component has no valid attributes, not rendering'); } return null; } return ; } export function MetaCharset({ charset = 'utf-8' }: { charset?: string } = {}) { return ; } export function MetaDescription({ description }: { description: string }) { return ; } export function MetaKeywords({ keywords }: { keywords: string | string[] }) { const keywordString = Array.isArray(keywords) ? keywords.join(', ') : keywords; return ; } export function MetaAuthor({ author }: { author: string }) { return ; } export function MetaThemeColor({ color, media }: { color: string; media?: string; }) { return ; } export function MetaViewport({ width = 'device-width', initialScale = 1, maximumScale, userScalable = true }: { width?: string | number; initialScale?: number; maximumScale?: number; userScalable?: boolean; }) { const parts = [`width=${width}`, `initial-scale=${initialScale}`]; if (maximumScale !== undefined) { parts.push(`maximum-scale=${maximumScale}`); } if (!userScalable) { parts.push('user-scalable=no'); } return ; } export function MetaHttpEquiv({ httpEquiv, content }: { httpEquiv: | 'content-type' | 'default-style' | 'refresh' | 'x-ua-compatible' | 'content-security-policy'; content: string; }) { return ; } export function MetaOpenGraph({ type, title, description, image, url, siteName }: { type?: 'website' | 'article' | 'product' | string; title?: string; description?: string; image?: string; url?: string; siteName?: string; }) { return ( <> {type && } {title && } {description && } {image && } {url && } {siteName && } ); } export function MetaTwitterCard({ card = 'summary', site, creator, title, description, image }: { card?: 'summary' | 'summary_large_image' | 'app' | 'player'; site?: string; creator?: string; title?: string; description?: string; image?: string; }) { return ( <> {site && } {creator && } {title && } {description && } {image && } ); } export function MetaRobots({ index = true, follow = true, noarchive = false, nosnippet = false }: { index?: boolean; follow?: boolean; noarchive?: boolean; nosnippet?: boolean; }) { const directives = [ index ? 'index' : 'noindex', follow ? 'follow' : 'nofollow' ]; if (noarchive) directives.push('noarchive'); if (nosnippet) directives.push('nosnippet'); return ; } ================================================ FILE: packages/evershop/src/components/common/Notification.scss ================================================ .Toastify__toast-container { z-index: 9999; -webkit-transform: translate3d(0, 0, 9999px); position: fixed; padding: 4px; width: 320px; box-sizing: border-box; color: #fff; background-color: transparent; } .Toastify__toast-container--top-left { top: 1em; left: 1em; } .Toastify__toast-container--top-center { top: 1em; left: 50%; transform: translateX(-50%); } .Toastify__toast-container--top-right { top: 1em; right: 1em; } .Toastify__toast-container--bottom-left { bottom: 1em; left: 1em; } .Toastify__toast-container--bottom-center { bottom: 1em; left: 50%; transform: translateX(-50%); } .Toastify__toast-container--bottom-right { bottom: 1em; right: 1em; } @media only screen and (max-width: 480px) { .Toastify__toast-container { width: 100vw; padding: 0; left: 0; margin: 0; } .Toastify__toast-container--top-left, .Toastify__toast-container--top-center, .Toastify__toast-container--top-right { top: 0; transform: translateX(0); } .Toastify__toast-container--bottom-left, .Toastify__toast-container--bottom-center, .Toastify__toast-container--bottom-right { bottom: 0; transform: translateX(0); } .Toastify__toast-container--rtl { right: 0; left: initial; } } .Toastify__toast { position: relative; min-height: 64px; box-sizing: border-box; margin-bottom: 0.625rem; padding: 8px; border-radius: 5px; display: -ms-flexbox; display: flex; -ms-flex-pack: justify; justify-content: space-between; max-height: 800px; overflow: hidden; font-family: sans-serif; cursor: pointer; direction: ltr; } .Toastify__toast--rtl { direction: rtl; } .Toastify__toast--dark { background: #121212; color: #fff; } .Toastify__toast--default { background: #fff; color: #aaa; } .Toastify__toast--info { background: #3498db; } .Toastify__toast--success { background: var(--success); } .Toastify__toast--warning { background: #f1c40f; } .Toastify__toast--error { background: var(--critical); } .Toastify__toast-body { margin: auto 0; -ms-flex: 1 1 auto; flex: 1 1 auto; } @media only screen and (max-width: 480px) { .Toastify__toast { margin-bottom: 0; } } .Toastify__close-button { color: #fff; background: transparent; outline: none; border: none; padding: 0; cursor: pointer; opacity: 0.7; transition: 0.3s ease; -ms-flex-item-align: start; align-self: center; } .Toastify__close-button--default { color: #000; opacity: 0.3; } .Toastify__close-button > svg { fill: currentColor; height: 16px; width: 14px; } .Toastify__close-button:hover, .Toastify__close-button:focus { opacity: 1; } @keyframes Toastify__trackProgress { 0% { transform: scaleX(1); } 100% { transform: scaleX(0); } } .Toastify__progress-bar { position: absolute; bottom: 0; left: 0; width: 100%; height: 5px; z-index: 9999; opacity: 0.7; background-color: rgba(255, 255, 255, 0.7); transform-origin: left; } .Toastify__progress-bar--animated { animation: Toastify__trackProgress linear 1 forwards; } .Toastify__progress-bar--controlled { transition: transform 0.2s; } .Toastify__progress-bar--rtl { right: 0; left: initial; transform-origin: right; } .Toastify__progress-bar--default { background: linear-gradient( to right, #4cd964, #5ac8fa, #007aff, #34aadc, #5856d6, #ff2d55 ); } .Toastify__progress-bar--dark { background: #bb86fc; } @keyframes Toastify__bounceInRight { from, 60%, 75%, 90%, to { animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } from { opacity: 0; transform: translate3d(3000px, 0, 0); } 60% { opacity: 1; transform: translate3d(-25px, 0, 0); } 75% { transform: translate3d(10px, 0, 0); } 90% { transform: translate3d(-5px, 0, 0); } to { transform: none; } } @keyframes Toastify__bounceOutRight { 20% { opacity: 1; transform: translate3d(-20px, 0, 0); } to { opacity: 0; transform: translate3d(2000px, 0, 0); } } @keyframes Toastify__bounceInLeft { from, 60%, 75%, 90%, to { animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } 0% { opacity: 0; transform: translate3d(-3000px, 0, 0); } 60% { opacity: 1; transform: translate3d(25px, 0, 0); } 75% { transform: translate3d(-10px, 0, 0); } 90% { transform: translate3d(5px, 0, 0); } to { transform: none; } } @keyframes Toastify__bounceOutLeft { 20% { opacity: 1; transform: translate3d(20px, 0, 0); } to { opacity: 0; transform: translate3d(-2000px, 0, 0); } } @keyframes Toastify__bounceInUp { from, 60%, 75%, 90%, to { animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } from { opacity: 0; transform: translate3d(0, 3000px, 0); } 60% { opacity: 1; transform: translate3d(0, -20px, 0); } 75% { transform: translate3d(0, 10px, 0); } 90% { transform: translate3d(0, -5px, 0); } to { transform: translate3d(0, 0, 0); } } @keyframes Toastify__bounceOutUp { 20% { transform: translate3d(0, -10px, 0); } 40%, 45% { opacity: 1; transform: translate3d(0, 20px, 0); } to { opacity: 0; transform: translate3d(0, -2000px, 0); } } @keyframes Toastify__bounceInDown { from, 60%, 75%, 90%, to { animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } 0% { opacity: 0; transform: translate3d(0, -3000px, 0); } 60% { opacity: 1; transform: translate3d(0, 25px, 0); } 75% { transform: translate3d(0, -10px, 0); } 90% { transform: translate3d(0, 5px, 0); } to { transform: none; } } @keyframes Toastify__bounceOutDown { 20% { transform: translate3d(0, 10px, 0); } 40%, 45% { opacity: 1; transform: translate3d(0, -20px, 0); } to { opacity: 0; transform: translate3d(0, 2000px, 0); } } .Toastify__bounce-enter--top-left, .Toastify__bounce-enter--bottom-left { animation-name: Toastify__bounceInLeft; } .Toastify__bounce-enter--top-right, .Toastify__bounce-enter--bottom-right { animation-name: Toastify__bounceInRight; } .Toastify__bounce-enter--top-center { animation-name: Toastify__bounceInDown; } .Toastify__bounce-enter--bottom-center { animation-name: Toastify__bounceInUp; } .Toastify__bounce-exit--top-left, .Toastify__bounce-exit--bottom-left { animation-name: Toastify__bounceOutLeft; } .Toastify__bounce-exit--top-right, .Toastify__bounce-exit--bottom-right { animation-name: Toastify__bounceOutRight; } .Toastify__bounce-exit--top-center { animation-name: Toastify__bounceOutUp; } .Toastify__bounce-exit--bottom-center { animation-name: Toastify__bounceOutDown; } @keyframes Toastify__zoomIn { from { opacity: 0; transform: scale3d(0.3, 0.3, 0.3); } 50% { opacity: 1; } } @keyframes Toastify__zoomOut { from { opacity: 1; } 50% { opacity: 0; transform: scale3d(0.3, 0.3, 0.3); } to { opacity: 0; } } .Toastify__zoom-enter { animation-name: Toastify__zoomIn; } .Toastify__zoom-exit { animation-name: Toastify__zoomOut; } @keyframes Toastify__flipIn { from { transform: perspective(400px) rotate3d(1, 0, 0, 90deg); animation-timing-function: ease-in; opacity: 0; } 40% { transform: perspective(400px) rotate3d(1, 0, 0, -20deg); animation-timing-function: ease-in; } 60% { transform: perspective(400px) rotate3d(1, 0, 0, 10deg); opacity: 1; } 80% { transform: perspective(400px) rotate3d(1, 0, 0, -5deg); } to { transform: perspective(400px); } } @keyframes Toastify__flipOut { from { transform: perspective(400px); } 30% { transform: perspective(400px) rotate3d(1, 0, 0, -20deg); opacity: 1; } to { transform: perspective(400px) rotate3d(1, 0, 0, 90deg); opacity: 0; } } .Toastify__flip-enter { animation-name: Toastify__flipIn; } .Toastify__flip-exit { animation-name: Toastify__flipOut; } @keyframes Toastify__slideInRight { from { transform: translate3d(110%, 0, 0); visibility: visible; } to { transform: translate3d(0, 0, 0); } } @keyframes Toastify__slideInLeft { from { transform: translate3d(-110%, 0, 0); visibility: visible; } to { transform: translate3d(0, 0, 0); } } @keyframes Toastify__slideInUp { from { transform: translate3d(0, 110%, 0); visibility: visible; } to { transform: translate3d(0, 0, 0); } } @keyframes Toastify__slideInDown { from { transform: translate3d(0, -110%, 0); visibility: visible; } to { transform: translate3d(0, 0, 0); } } @keyframes Toastify__slideOutRight { from { transform: translate3d(0, 0, 0); } to { visibility: hidden; transform: translate3d(110%, 0, 0); } } @keyframes Toastify__slideOutLeft { from { transform: translate3d(0, 0, 0); } to { visibility: hidden; transform: translate3d(-110%, 0, 0); } } @keyframes Toastify__slideOutDown { from { transform: translate3d(0, 0, 0); } to { visibility: hidden; transform: translate3d(0, 500px, 0); } } @keyframes Toastify__slideOutUp { from { transform: translate3d(0, 0, 0); } to { visibility: hidden; transform: translate3d(0, -500px, 0); } } .Toastify__slide-enter--top-left, .Toastify__slide-enter--bottom-left { animation-name: Toastify__slideInLeft; } .Toastify__slide-enter--top-right, .Toastify__slide-enter--bottom-right { animation-name: Toastify__slideInRight; } .Toastify__slide-enter--top-center { animation-name: Toastify__slideInDown; } .Toastify__slide-enter--bottom-center { animation-name: Toastify__slideInUp; } .Toastify__slide-exit--top-left, .Toastify__slide-exit--bottom-left { animation-name: Toastify__slideOutLeft; } .Toastify__slide-exit--top-right, .Toastify__slide-exit--bottom-right { animation-name: Toastify__slideOutRight; } .Toastify__slide-exit--top-center { animation-name: Toastify__slideOutUp; } .Toastify__slide-exit--bottom-center { animation-name: Toastify__slideOutDown; } ================================================ FILE: packages/evershop/src/components/common/Notification.tsx ================================================ import { useAppState } from '@components/common/context/app.js'; import { get } from '@evershop/evershop/lib/util/get'; import React from 'react'; import { toast, ToastContainer } from 'react-toastify'; import './Notification.scss'; export default function Notification() { const notify = (type, message) => { switch (type) { case 'success': toast.success(message); break; case 'error': toast.error(message); break; case 'info': toast.info(message); break; case 'warning': toast.warning(message); break; default: toast(message); } }; const context = useAppState(); React.useEffect(() => { get(context, 'notifications', []).forEach((n) => notify(n.type, n.message)); }, []); return (
    ); } ================================================ FILE: packages/evershop/src/components/common/ProductNoThumbnail.tsx ================================================ import React from 'react'; const ProductNoThumbnail: React.FC<{ width?: number; height?: number; className?: string; }> = ({ width, height, className }) => { return ( ); }; export { ProductNoThumbnail }; ================================================ FILE: packages/evershop/src/components/common/RenderIfTrue.tsx ================================================ import React from 'react'; interface RenderIfTrueProps { condition: boolean; children: React.ReactNode; } export default function RenderIfTrue({ condition, children }: RenderIfTrueProps) { return condition === true ? children : null; } ================================================ FILE: packages/evershop/src/components/common/Script.tsx ================================================ /* eslint-disable no-console */ import React from 'react'; interface BaseScriptProps { src?: string; async?: boolean; defer?: boolean; type?: | 'text/javascript' | 'module' | 'importmap' | 'speculationrules' | 'application/json' | 'application/ld+json' | string; crossOrigin?: 'anonymous' | 'use-credentials'; integrity?: string; nonce?: string; referrerPolicy?: | 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url'; noModule?: boolean; fetchPriority?: 'high' | 'low' | 'auto'; blocking?: 'render'; attributionSrc?: boolean | string; children?: React.ReactNode; } interface ExternalScriptProps extends BaseScriptProps { src: string; children?: never; } interface InlineScriptProps extends BaseScriptProps { src?: never; children: React.ReactNode; } type ScriptProps = ExternalScriptProps | InlineScriptProps; const VALID_REFERRER_POLICIES = [ 'no-referrer', 'no-referrer-when-downgrade', 'origin', 'origin-when-cross-origin', 'same-origin', 'strict-origin', 'strict-origin-when-cross-origin', 'unsafe-url' ] as const; const VALID_CROSSORIGIN_VALUES = ['anonymous', 'use-credentials'] as const; function validateScriptProps(props: any): { isValid: boolean; errors: string[]; } { const errors: string[] = []; if (!props.src && !props.children) { errors.push('Script must have either src attribute or children content'); } if (props.src && props.children) { errors.push('Script cannot have both src attribute and children content'); } if (props.async && props.defer) { errors.push('Script cannot have both async and defer attributes'); } if (!props.src && props.async) { errors.push('async attribute has no effect on inline scripts'); } if (!props.src && props.defer) { errors.push('defer attribute has no effect on inline scripts'); } if ( props.referrerPolicy && !VALID_REFERRER_POLICIES.includes(props.referrerPolicy) ) { errors.push(`Invalid referrerPolicy: ${props.referrerPolicy}`); } if ( props.crossOrigin && !VALID_CROSSORIGIN_VALUES.includes(props.crossOrigin) ) { errors.push(`Invalid crossOrigin: ${props.crossOrigin}`); } if (props.integrity && !props.src) { errors.push('integrity attribute requires src attribute'); } if ( props.fetchPriority && !['high', 'low', 'auto'].includes(props.fetchPriority) ) { errors.push(`Invalid fetchPriority: ${props.fetchPriority}`); } if (props.blocking && props.blocking !== 'render') { errors.push('blocking attribute can only be "render"'); } return { isValid: errors.length === 0, errors }; } function sanitizeScriptProps(props: any): Record { const allowedAttributes = [ 'src', 'async', 'defer', 'type', 'crossOrigin', 'integrity', 'nonce', 'referrerPolicy', 'noModule', 'fetchPriority', 'blocking', 'attributionSrc' ]; const sanitized = Object.keys(props) .filter( (key) => allowedAttributes.includes(key) && props[key] !== undefined && props[key] !== null ) .reduce((obj, key) => { if (typeof props[key] === 'boolean') { if (props[key]) { obj[key] = key === 'attributionSrc' && props[key] === true ? '' : props[key]; } } else { obj[key] = String(props[key]).trim(); } return obj; }, {} as Record); return sanitized; } export function Script(props: ScriptProps) { if (process.env.NODE_ENV === 'development') { const validation = validateScriptProps(props); if (!validation.isValid) { console.error('Script component validation errors:', validation.errors); validation.errors.forEach((error) => console.error(`Script: ${error}`)); } } if (!props.src && !props.children) { if (process.env.NODE_ENV === 'development') { console.warn('Script component has no src or children, not rendering'); } return null; } const sanitizedProps = sanitizeScriptProps(props); if (props.src) { return ; } export function ScriptExternal({ src, async = false, defer = false, crossOrigin, integrity, referrerPolicy, fetchPriority = 'auto', nonce }: { src: string; async?: boolean; defer?: boolean; crossOrigin?: 'anonymous' | 'use-credentials'; integrity?: string; referrerPolicy?: | 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url'; fetchPriority?: 'high' | 'low' | 'auto'; nonce?: string; }) { return ( ); } export function ScriptInline({ children, type = 'text/javascript', nonce }: { children: React.ReactNode; type?: string; nonce?: string; }) { return ( ); } export function ScriptJSON({ id, data, nonce }: { id?: string; data: any; nonce?: string; }) { return ( ); } export function ScriptImportMap({ imports, scopes, nonce }: { imports?: Record; scopes?: Record>; nonce?: string; }) { const importMap: any = {}; if (imports) importMap.imports = imports; if (scopes) importMap.scopes = scopes; return ( ); } export function ScriptNoModule({ src, children, async = false, defer = false }: { src?: string; children?: React.ReactNode; async?: boolean; defer?: boolean; }) { if (src && children) { console.error('ScriptNoModule cannot have both src and children'); return null; } if (src) { return ); } ================================================ FILE: packages/evershop/src/components/common/SimplePagination.tsx ================================================ import { ChevronLeft, ChevronRight } from 'lucide-react'; import React from 'react'; interface SimplePaginationProps { total: number; count: number; page: number; hasNext: boolean; setPage: (page: number) => void; } export function SimplePagination({ total, count, page, hasNext, setPage }: SimplePaginationProps) { return (
    {count} of {total}
    {page > 1 && ( { e.preventDefault(); setPage(page - 1); }} > )} {page === 1 && ( )} {hasNext && ( { e.preventDefault(); setPage(page + 1); }} > )} {!hasNext && ( )}
    ); } ================================================ FILE: packages/evershop/src/components/common/StaticImage.tsx ================================================ import { useAppState } from '@components/common/context/app.js'; import { Image, ImageProps } from '@components/common/Image.js'; import React, { useMemo } from 'react'; export interface StaticImageProps extends Omit { subPath: string; // Path relative to the root public folder or the public folder of the active theme } export const StaticImage: React.FC = ({ subPath, quality = 75, ...props }) => { const { config } = useAppState(); const baseUrl = config?.pageMeta?.baseUrl || ''; const imagePath = useMemo(() => { const formattedSubPath = subPath.startsWith('/') ? subPath.substring(1) : subPath; return `${baseUrl}/assets/${formattedSubPath}`; }, [baseUrl, subPath]); return ; }; ================================================ FILE: packages/evershop/src/components/common/Title.tsx ================================================ /* eslint-disable no-console */ import React from 'react'; /** * Props for the Title component */ export interface TitleProps extends Omit, 'children'> { /** * The text content for the document title. * Should be descriptive and unique for SEO purposes. */ title: string; /** * Optional prefix to add to the title (e.g., site name). * Will be separated from the main title with a separator. */ prefix?: string; /** * Optional suffix to add to the title (e.g., site name). * Will be separated from the main title with a separator. */ suffix?: string; /** * Separator to use between title parts. * @default " - " */ separator?: string; /** * Maximum length for the title (SEO best practice: ~55-60 chars). * If exceeded, will truncate and add ellipsis. */ maxLength?: number; } /** * Validates title content for SEO and accessibility best practices */ const validateTitle = (title: string, maxLength?: number): void => { if (process.env.NODE_ENV === 'development') { // Check for empty title if (!title || title.trim().length === 0) { console.warn( 'Title: Empty title detected. This is bad for SEO and accessibility.' ); return; } // Check for very short titles if (title.length < 3) { console.warn( 'Title: Very short title detected. Consider making it more descriptive.' ); } // Check for very long titles const recommendedMaxLength = maxLength || 60; if (title.length > recommendedMaxLength) { console.warn( `Title: Title exceeds recommended length of ${recommendedMaxLength} characters (${title.length}). May be truncated in search results.` ); } // Check for keyword stuffing patterns const words = title.toLowerCase().split(/\s+/); const wordCount = words.reduce((acc, word) => { acc[word] = (acc[word] || 0) + 1; return acc; }, {} as Record); const repeatedWords = Object.entries(wordCount).filter( ([word, count]) => count > 2 && word.length > 3 ); if (repeatedWords.length > 0) { console.warn( 'Title: Potential keyword stuffing detected. Repeated words:', repeatedWords.map(([word]) => word).join(', ') ); } // Check for common bad patterns if (title.includes('||') || title.includes('>>') || title.includes('<<')) { console.warn( 'Title: Unusual separators detected. Consider using standard separators like " - " or " | ".' ); } // Check if title starts/ends with separator characters if (/^[-|•·]|\s[-|•·]\s*$/.test(title)) { console.warn( 'Title: Title appears to start or end with separator characters.' ); } } }; /** * Formats the complete title string with optional prefix/suffix */ const formatTitle = ( title: string, prefix?: string, suffix?: string, separator: string = ' - ', maxLength?: number ): string => { const parts: string[] = []; if (prefix) parts.push(prefix); parts.push(title); if (suffix) parts.push(suffix); let formattedTitle = parts.join(separator); // Truncate if needed if (maxLength && formattedTitle.length > maxLength) { // Try to truncate at word boundaries const truncated = formattedTitle.substring(0, maxLength - 3); const lastSpace = truncated.lastIndexOf(' '); if (lastSpace > formattedTitle.length * 0.7) { formattedTitle = truncated.substring(0, lastSpace) + '...'; } else { formattedTitle = truncated + '...'; } } return formattedTitle; }; /** * Title component that renders an HTML element with SEO and accessibility best practices. * * The title element is crucial for: * - SEO: Search engines use it as the clickable headline in search results * - Accessibility: Screen readers announce the page title when users navigate to the page * - User Experience: Displayed in browser tabs and bookmarks * * SEO Best Practices: * - Keep titles between 30-60 characters (55-60 optimal for Google) * - Make each page title unique within your site * - Put important keywords first * - Avoid keyword stuffing * - Use descriptive, readable titles that entice clicks * * @example * // Basic usage * <Title title="About Us" /> * * @example * // With site branding * <Title * title="Product Details" * suffix="EverShop" * separator=" | " * /> * * @example * // E-commerce product page * <Title * title="iPhone 14 Pro Max - 256GB Space Black" * suffix="TechStore" * maxLength={60} * /> * * @example * // Blog post * <Title * title="10 Tips for Better React Performance" * suffix="Developer Blog" * /> * * @example * // Error page * <Title * title="Page Not Found (404)" * suffix="EverShop" * /> */ export function Title({ title, prefix, suffix, separator = ' - ', maxLength, ...otherProps }: TitleProps): React.ReactElement { // Format the complete title const formattedTitle = formatTitle( title, prefix, suffix, separator, maxLength ); // Validate in development validateTitle(formattedTitle, maxLength); return <title {...otherProps}>{formattedTitle}; } /** * Convenience component for product page titles */ export function ProductTitle({ productName, category, brand, siteName, separator = ' - ', maxLength = 60, ...props }: Omit & { productName: string; category?: string; brand?: string; siteName?: string; }): React.ReactElement { const titleParts: string[] = [productName]; if (category) titleParts.push(category); if (brand) titleParts.push(brand); const title = titleParts.join(' '); return ( ); } /** * Convenience component for category/collection page titles */ export function CategoryTitle({ categoryName, itemCount, siteName, separator = ' - ', maxLength = 60, ...props }: Omit<TitleProps, 'title'> & { categoryName: string; itemCount?: number; siteName?: string; }): React.ReactElement { let title = categoryName; if (itemCount !== undefined) { title += ` (${itemCount} items)`; } return ( <Title title={title} suffix={siteName} separator={separator} maxLength={maxLength} {...props} /> ); } /** * Convenience component for error page titles */ export function ErrorTitle({ errorCode, errorMessage, siteName, separator = ' - ', ...props }: Omit<TitleProps, 'title'> & { errorCode: number | string; errorMessage?: string; siteName?: string; }): React.ReactElement { const title = errorMessage ? `${errorMessage} (${errorCode})` : `Error ${errorCode}`; return ( <Title title={title} suffix={siteName} separator={separator} {...props} /> ); } /** * Convenience component for search result page titles */ export function SearchTitle({ query, resultCount, siteName, separator = ' - ', maxLength = 60, ...props }: Omit<TitleProps, 'title'> & { query: string; resultCount?: number; siteName?: string; }): React.ReactElement { let title = `Search: ${query}`; if (resultCount !== undefined) { title += ` (${resultCount} results)`; } return ( <Title title={title} suffix={siteName} separator={separator} maxLength={maxLength} {...props} /> ); } ================================================ FILE: packages/evershop/src/components/common/context/app.tsx ================================================ import { produce } from 'immer'; import React, { useMemo } from 'react'; import { AppContextDispatchValue, AppStateContextValue } from '../../../types/appContext.js'; const AppStateContext = React.createContext<AppStateContextValue>( {} as AppStateContextValue ); const AppContextDispatch = React.createContext<AppContextDispatchValue>( {} as AppContextDispatchValue ); interface AppProviderProps { value: AppStateContextValue; children: React.ReactNode; } export function AppProvider({ value, children }: AppProviderProps) { const [data, setData] = React.useState<AppStateContextValue>(value); const [fetching, setFetching] = React.useState<boolean>(false); const fetchPageData = async (url: string | URL): Promise<void> => { setFetching(true); try { const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json' } }); const dataResponse = await response.json(); // Update the entire context using immer setData( produce(data, (draft) => { Object.assign(draft, dataResponse.eContext); return draft; }) ); } catch (error) { } finally { setFetching(false); } }; React.useEffect(() => { window.onpopstate = async () => { // Get the current url const url = new URL(window.location.href, window.location.origin); url.searchParams.append('ajax', 'true'); await fetchPageData(url.toString()); }; }, []); const contextDispatchValue = useMemo<AppContextDispatchValue>( () => ({ setData, fetchPageData }), [setData, fetchPageData] ); const contextValue = useMemo<AppStateContextValue>( () => ({ ...data, fetching }), [data, fetching] ); return ( <AppContextDispatch.Provider value={contextDispatchValue}> <AppStateContext.Provider value={contextValue}> {children} </AppStateContext.Provider> </AppContextDispatch.Provider> ); } export const useAppState = (): AppStateContextValue => React.useContext(AppStateContext); export const useAppDispatch = (): AppContextDispatchValue => React.useContext(AppContextDispatch); ================================================ FILE: packages/evershop/src/components/common/customer/address/AddressSummary.jsx ================================================ /* eslint-disable react/prop-types */ import Area from '@components/common/Area'; import React from 'react'; export function AddressSummary({ address }) { return ( <Area id="addressSummary" className="address__summary" coreComponents={[ { component: { default: ({ fullName }) => ( <div className="full-name">{fullName}</div> ) }, props: { fullName: address.fullName }, sortOrder: 10, id: 'fullName' }, { component: { default: ({ address1 }) => ( <div className="address-one">{address1}</div> ) }, props: { address1: address.address1 }, sortOrder: 20, id: 'address1' }, { component: { default: ({ city, province, postcode, country }) => ( <div className="city-province-postcode"> <div>{`${postcode}, ${city}`}</div> <div> {province && <span>{province.name}, </span>}{' '} <span>{country.name}</span> </div> </div> ) }, props: { city: address.city, province: address.province, postcode: address.postcode, country: address.country }, sortOrder: 40, id: 'cityProvincePostcode' }, { component: { default: ({ telephone }) => ( <div className="telephone">{telephone}</div> ) }, props: { telephone: address.telephone }, sortOrder: 60, id: 'telephone' } ]} /> ); } ================================================ FILE: packages/evershop/src/components/common/form/CheckboxField.tsx ================================================ import { Tooltip } from '@components/common/form/Tooltip.js'; import { getNestedError } from '@components/common/form/utils/getNestedError.js'; import { Checkbox } from '@components/common/ui/Checkbox.js'; import { Field, FieldError, FieldLabel, FieldLegend } from '@components/common/ui/Field.js'; import { Label } from '@components/common/ui/Label.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; import { useFormContext, RegisterOptions, FieldPath, FieldValues, Controller } from 'react-hook-form'; interface CheckboxOption { value: string | number; label: string; disabled?: boolean; } interface CheckboxFieldProps<T extends FieldValues = FieldValues> extends Omit< React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'type' | 'defaultValue' > { name: FieldPath<T>; label?: string; error?: string; helperText?: string; required?: boolean; validation?: RegisterOptions<T>; options?: CheckboxOption[]; defaultValue?: boolean | (string | number)[]; direction?: 'horizontal' | 'vertical'; wrapperClassName?: string; } export function CheckboxField<T extends FieldValues = FieldValues>({ name, label, error, wrapperClassName, helperText, required, validation, options, defaultValue, direction = 'vertical', className, disabled, ...props }: CheckboxFieldProps<T>) { const { control, formState: { errors } } = useFormContext<T>(); const fieldError = getNestedError(name, errors, error); const fieldId = `field-${name}`; const validationRules = { ...validation, ...(required && !validation?.required && { required: _('${field} is required', { field: label || name }) }) }; const containerClass = direction === 'horizontal' ? 'checkbox-group horizontal' : 'checkbox-group'; if (!options || options.length === 0) { return ( <Field data-invalid={fieldError ? 'true' : 'false'} className={wrapperClassName} > <div className="flex items-center gap-2"> <Controller name={name} control={control} rules={validationRules} defaultValue={defaultValue as any} render={({ field }) => ( <Checkbox id={fieldId} checked={!!field.value} onCheckedChange={(checked) => field.onChange(checked)} onBlur={field.onBlur} disabled={disabled} className={className} aria-invalid={fieldError !== undefined ? 'true' : 'false'} aria-describedby={ fieldError !== undefined ? `${fieldId}-error` : helperText ? `${fieldId}-helper` : undefined } /> )} /> {label && ( <FieldLabel htmlFor={fieldId} className="text-sm font-normal cursor-pointer" > {label} {required && <span className="text-destructive">*</span>} {helperText && <Tooltip content={helperText} position="top" />} </FieldLabel> )} </div> {fieldError && <FieldError>{fieldError}</FieldError>} </Field> ); } return ( <Field data-invalid={fieldError ? 'true' : 'false'} className={wrapperClassName} > {label && ( <fieldset> <FieldLegend> <> {label} {required && <span className="text-destructive">*</span>} {helperText && <Tooltip content={helperText} position="top" />} </> </FieldLegend> <Controller name={name} control={control} rules={validationRules} defaultValue={defaultValue as any} render={({ field }) => ( <div className={containerClass}> {options.map((option, index) => { const isChecked = Array.isArray(field.value) ? field.value.includes(option.value) : false; return ( <div key={option.value} className="flex items-center gap-2"> <Checkbox id={`${fieldId}-${index}`} disabled={disabled || option.disabled} checked={isChecked} onCheckedChange={(checked) => { const currentValues = Array.isArray(field.value) ? field.value : []; if (checked) { field.onChange([...currentValues, option.value]); } else { field.onChange( currentValues.filter( (val) => val !== option.value ) ); } }} onBlur={field.onBlur} className={className} aria-invalid={fieldError ? 'true' : 'false'} aria-describedby={ fieldError ? `${fieldId}-error` : undefined } /> <Label htmlFor={`${fieldId}-${index}`} className={`text-sm cursor-pointer ${ option.disabled ? 'opacity-50 cursor-not-allowed' : '' }`} > {option.label} </Label> </div> ); })} </div> )} /> </fieldset> )} {fieldError && <FieldError>{fieldError}</FieldError>} </Field> ); } ================================================ FILE: packages/evershop/src/components/common/form/DateField.tsx ================================================ import { Tooltip } from '@components/common/form/Tooltip.js'; import { getNestedError } from '@components/common/form/utils/getNestedError.js'; import { Field, FieldError, FieldLabel } from '@components/common/ui/Field.js'; import { InputGroup, InputGroupInput } from '@components/common/ui/InputGroup.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; import { useFormContext, RegisterOptions, FieldPath, FieldValues, Controller } from 'react-hook-form'; interface DateFieldProps<T extends FieldValues = FieldValues> extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'type'> { name: FieldPath<T>; label?: string; error?: string; helperText?: string; required?: boolean; validation?: RegisterOptions<T>; wrapperClassName?: string; } export function DateField<T extends FieldValues = FieldValues>({ name, label, error, wrapperClassName, helperText, required, validation, className, min, max, defaultValue, ...props }: DateFieldProps<T>) { const { control, formState: { errors } } = useFormContext<T>(); const fieldError = getNestedError(name, errors, error); const fieldId = `field-${name}`; const { valueAsNumber, ...cleanValidation } = validation || {}; const validationRules = { ...cleanValidation, ...(required && { required: _('${field} is required', { field: label || name }) }), validate: { ...validation?.validate, minDate: (value) => { if (!min || !value) return true; return ( value >= min || _('Date must be after ${min}', { min: min.toString() }) ); }, maxDate: (value) => { if (!max || !value) return true; return ( value <= max || _('Date must be before ${max}', { max: max.toString() }) ); } } }; return ( <Field data-invalid={fieldError ? 'true' : 'false'} className={wrapperClassName} > {label && ( <FieldLabel htmlFor={fieldId}> <> {label} {required && <span className="text-destructive">*</span>} {helperText && <Tooltip content={helperText} position="top" />} </> </FieldLabel> )} <Controller name={name} control={control} defaultValue={defaultValue as any} rules={validationRules} render={({ field }) => ( <InputGroup> <InputGroupInput {...field} value={field.value ?? ''} id={fieldId} type="date" min={min} max={max} className={className} aria-invalid={fieldError !== undefined ? 'true' : 'false'} aria-describedby={ fieldError !== undefined ? `${fieldId}-error` : undefined } {...props} /> </InputGroup> )} /> {fieldError && <FieldError>{fieldError}</FieldError>} </Field> ); } ================================================ FILE: packages/evershop/src/components/common/form/DateTimeLocalField.tsx ================================================ import { Tooltip } from '@components/common/form/Tooltip.js'; import { getNestedError } from '@components/common/form/utils/getNestedError.js'; import { Field, FieldError, FieldLabel } from '@components/common/ui/Field.js'; import { InputGroup, InputGroupInput } from '@components/common/ui/InputGroup.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; import { useFormContext, RegisterOptions, FieldPath, FieldValues, Controller } from 'react-hook-form'; interface DateTimeLocalFieldProps<T extends FieldValues = FieldValues> extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'type'> { name: FieldPath<T>; label?: string; error?: string; helperText?: string; required?: boolean; validation?: RegisterOptions<T>; wrapperClassName?: string; } export function DateTimeLocalField<T extends FieldValues = FieldValues>({ name, label, error, wrapperClassName, helperText, required, validation, className, min, max, step, ...props }: DateTimeLocalFieldProps<T>) { const { control, formState: { errors } } = useFormContext<T>(); const fieldError = getNestedError(name, errors, error); const fieldId = `field-${name}`; const { valueAsNumber, valueAsDate, ...cleanValidation } = validation || {}; const validationRules = { ...cleanValidation, ...(required && { required: _('${field} is required', { field: label || name }) }), validate: { ...validation?.validate, minDateTime: (value) => { if (!min || !value) return true; return ( value >= min || _('Date and time must be after ${min}', { min: min.toString() }) ); }, maxDateTime: (value) => { if (!max || !value) return true; return ( value <= max || _('Date and time must be before ${max}', { max: max.toString() }) ); } } }; return ( <Field data-invalid={fieldError ? 'true' : 'false'} className={wrapperClassName} > {label && ( <FieldLabel htmlFor={fieldId}> <> {label} {required && <span className="text-destructive">*</span>} {helperText && <Tooltip content={helperText} position="top" />} </> </FieldLabel> )} <Controller name={name} control={control} rules={validationRules} render={({ field }) => ( <InputGroup> <InputGroupInput {...field} value={field.value ?? ''} id={fieldId} type="datetime-local" min={min} max={max} step={step} className={className} aria-invalid={fieldError !== undefined ? 'true' : 'false'} aria-describedby={ fieldError !== undefined ? `${fieldId}-error` : undefined } {...props} /> </InputGroup> )} /> {fieldError && <FieldError>{fieldError}</FieldError>} </Field> ); } ================================================ FILE: packages/evershop/src/components/common/form/Editor.scss ================================================ body .codex-editor { z-index: 99; } #rows { .row { background-color: #fff; } .row__container { &:hover, &:focus-within { z-index: 100 !important; } } .drag__icon { cursor: move; display: flex; align-items: center; justify-content: center; border-radius: 4px; &:hover { background-color: #e0e0e0; } svg { display: block; // Prevent extra space below svg } } } .item { padding: 10px; background: var(--divider); border: 2px solid var(--border); } #rows .draggable-source--is-dragging { background: var(--divider); color: var(--divider); } #rows .draggable-source--is-dragging > * { visibility: hidden; } .ce-rawtool__textarea { z-index: 0; min-height: 200px; width: 100%; padding: 12px; border: 1px solid #e5e7eb; border-radius: 6px; font-family: 'Courier New', monospace; font-size: 14px; line-height: 1.6; resize: vertical; &:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); } } .ce-rawtool { margin: 0.5em 0; } ================================================ FILE: packages/evershop/src/components/common/form/Editor.tsx ================================================ import { FileBrowser } from '@components/admin/FileBrowser.js'; import { getColumnClasses } from '@components/common/form/editor/GetColumnClasses.js'; import { getRowClasses } from '@components/common/form/editor/GetRowClasses.js'; import { RawToolWrapper } from '@components/common/form/editor/RawToolWrapper.js'; import { RowTemplates } from '@components/common/form/editor/RowTemplates.js'; import { Field, FieldLabel } from '@components/common/ui/Field.js'; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { CircleX } from 'lucide-react'; import React from 'react'; import { useFormContext } from 'react-hook-form'; import { v4 as uuidv4 } from 'uuid'; import './Editor.scss'; async function loadEditorJS(): Promise<any> { const { default: EditorJS } = await import('@editorjs/editorjs'); return EditorJS; } async function loadEditorJSImage(): Promise<any> { const { default: ImageTool } = await import('@evershop/editorjs-image'); return ImageTool; } async function loadEditorJSHeader(): Promise<any> { const { default: Header } = await import('@editorjs/header'); return Header; } async function loadEditorJSList(): Promise<any> { const { default: List } = await import('@editorjs/list'); return List; } async function loadEditorJSQuote(): Promise<any> { const { default: Quote } = await import('@editorjs/quote'); return Quote; } // Using custom RawToolWrapper instead to fix backspace issues // async function loadEditorJSRaw(): Promise<any> { // const { default: RawTool } = await import('@editorjs/raw'); // return RawTool; // } const SortableRow: React.FC<{ row: Row; removeRow: (rowId: string) => void; children: React.ReactNode; }> = ({ row, removeRow, children }) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: row.id }); const style = { transform: transform ? `translateY(${transform.y}px)` : undefined, transition, opacity: isDragging ? 0.5 : 1, position: 'relative' } as React.CSSProperties; return ( <div className="border border-border row__container mt-3 first:mt-0 rounded-md" id={row.id} ref={setNodeRef} style={style} > <div className="config p-3 flex justify-between bg-muted items-center"> <div className="drag__icon cursor-move" {...attributes} {...listeners}> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#949494" width={20} height={20} > <g> <path fill="none" d="M0 0h24v24H0z" /> <path fillRule="nonzero" d="M14 6h2v2h5a1 1 0 0 1 1 1v7.5L16 13l.036 8.062 2.223-2.15L20.041 22H9a1 1 0 0 1-1-1v-5H6v-2h2V9a1 1 0 0 1 1-1h5V6zm8 11.338V21a1 1 0 0 1-.048.307l-1.96-3.394L22 17.338zM4 14v2H2v-2h2zm0-4v2H2v-2h2zm0-4v2H2V6h2zm0-4v2H2V2h2zm4 0v2H6V2h2zm4 0v2h-2V2h2zm4 0v2h-2V2h2z" /> </g> </svg> </div> <div> <a href="#" onClick={(e) => { e.preventDefault(); removeRow(row.id); }} > <CircleX width={20} height={20} /> </a> </div> </div> {children} </div> ); }; export interface Row { id: string; size: number; columns: { id: string; size: number; data: any; }[]; } export interface EditorProps { name: string; value?: Row[]; label?: string; } export const Editor: React.FC<EditorProps> = ({ name, value = [], label }) => { const [openFileBrowser, setOpenFileBrowser] = React.useState(false); const [fileBrowser, setFileBrowser] = React.useState<{ onUpload: (fileUrl: string) => void; onError: (error: string) => void; } | null>(null); const { register, setValue } = useFormContext(); const [rows, setRows] = React.useState( value ? value.map((row) => { const rowId = `r__${uuidv4()}`; return { ...row, className: getRowClasses(row.size), id: row.id || rowId, columns: row.columns.map((column) => { const colId = `c__${uuidv4()}`; return { ...column, className: getColumnClasses(column.size), id: column.id || colId }; }) }; }) : [] ); const editors = React.useRef({}); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ); const handleDragEnd = (event) => { const { active, over } = event; if (active && over && active.id !== over.id) { setRows((items) => { const oldIndex = items.findIndex((row) => row.id === active.id); const newIndex = items.findIndex((row) => row.id === over.id); if (oldIndex !== -1 && newIndex !== -1) { return arrayMove(items, oldIndex, newIndex); } return items; }); } }; React.useEffect(() => { const initEditors = async () => { const EditorJS = await loadEditorJS(); const ImageTool = await loadEditorJSImage(); const Header = await loadEditorJSHeader(); const List = await loadEditorJSList(); const Quote = await loadEditorJSQuote(); // Using RawToolWrapper instead of loading from @editorjs/raw setValue(name, rows); rows.forEach((row) => { row.columns.forEach((column) => { if (!editors.current[column.id]) { editors.current[column.id] = {}; editors.current[column.id].instance = new EditorJS({ holder: column.id, placeholder: 'Type / to see the available blocks', minHeight: 0, tools: { header: Header, list: List, raw: { class: RawToolWrapper, inlineToolbar: false }, quote: Quote, image: { class: ImageTool, config: { onSelectFile: (onUpload, onError) => { setFileBrowser({ onUpload: (fileUrl) => { onUpload({ success: 1, file: { url: fileUrl } }); }, onError }); setOpenFileBrowser(true); } } } }, data: column.data, onChange: (api) => { api.saver.save().then((outputData) => { // Save outputData to the column and trigger re-render setRows((prevRows) => { const newRows = [...prevRows]; const rowIdx = newRows.findIndex((r) => r.id === row.id); const columnIdx = newRows[rowIdx].columns.findIndex( (c) => c.id === column.id ); newRows[rowIdx].columns[columnIdx].data = outputData; setValue(name, newRows); return newRows; }); }); } }); } }); }); }; initEditors(); }, [rows.length]); const removeRow = (rowId) => { setRows(rows.filter((i) => i.id !== rowId)); }; const addRow = (row) => { setRows(rows.concat(row)); }; return ( <Field className="editor form-field-container"> <FieldLabel htmlFor="description mt-4">{label}</FieldLabel> <div className="prose prose-xl max-w-none"> <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd} > <SortableContext items={rows.map((row) => row.id)} strategy={verticalListSortingStrategy} > <div id="rows"> {rows.map((row) => ( // Grid template columns based on the number of columns in the row <SortableRow key={row.id} row={row} removeRow={removeRow}> <div className={`row grid p-5 divide-x divide-dashed ${row.className}`} style={{ minHeight: '30px' }} > {row.columns.map((column) => ( <div className={`column p-3 ${column.className}`} key={column.id} > <div id={column.id} /> </div> ))} </div> </SortableRow> ))} </div> </SortableContext> </DndContext> <div className="flex justify-center"> <div className="flex justify-center flex-col mt-5"> <RowTemplates addRow={addRow} /> </div> </div> </div> <input type="hidden" {...register(name)} /> {openFileBrowser && ( <FileBrowser onInsert={(url) => { fileBrowser && fileBrowser.onUpload(url); setOpenFileBrowser(false); }} close={() => setOpenFileBrowser(false)} isMultiple={false} /> )} </Field> ); }; ================================================ FILE: packages/evershop/src/components/common/form/EmailField.tsx ================================================ import { Tooltip } from '@components/common/form/Tooltip.js'; import { getNestedError } from '@components/common/form/utils/getNestedError.js'; import { Field, FieldError, FieldLabel } from '@components/common/ui/Field.js'; import { InputGroup, InputGroupAddon, InputGroupInput } from '@components/common/ui/InputGroup.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; import { useFormContext, RegisterOptions, FieldPath, FieldValues, Controller } from 'react-hook-form'; interface EmailFieldProps<T extends FieldValues = FieldValues> extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'type'> { name: FieldPath<T>; label?: string; error?: string; helperText?: string; required?: boolean; validation?: RegisterOptions<T>; wrapperClassName?: string; prefixIcon?: React.ReactNode; suffixIcon?: React.ReactNode; } export function EmailField<T extends FieldValues = FieldValues>({ name, label, error, helperText, required, validation, wrapperClassName, className, defaultValue, prefixIcon, suffixIcon, ...props }: EmailFieldProps<T>) { const { control, formState: { errors } } = useFormContext<T>(); const fieldError = getNestedError(name, errors, error); const fieldId = `field-${name}`; const validationRules = { ...validation, ...(required && !validation?.required && { required: _('${field} is required', { field: label || name }) }), pattern: validation?.pattern || { value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, message: _('Please enter a valid email address') } }; const renderInput = () => ( <Controller name={name} control={control} defaultValue={defaultValue as any} rules={validationRules} render={({ field }) => ( <InputGroupInput {...field} value={field.value ?? ''} id={fieldId} type="email" aria-invalid={fieldError !== undefined ? 'true' : 'false'} aria-describedby={ fieldError !== undefined ? `${fieldId}-error` : undefined } {...props} /> )} /> ); return ( <Field data-invalid={fieldError ? 'true' : 'false'} className={wrapperClassName} > {label && ( <FieldLabel htmlFor={fieldId}> <> {label} {required && <span className="text-destructive">*</span>} {helperText && <Tooltip content={helperText} position="top" />} </> </FieldLabel> )} <InputGroup> {renderInput()} {prefixIcon && ( <InputGroupAddon align={'inline-start'}>{prefixIcon}</InputGroupAddon> )} {suffixIcon && ( <InputGroupAddon align={'inline-end'}>{suffixIcon}</InputGroupAddon> )} </InputGroup> {fieldError && <FieldError>{fieldError}</FieldError>} </Field> ); } ================================================ FILE: packages/evershop/src/components/common/form/FileField.tsx ================================================ import { Tooltip } from '@components/common/form/Tooltip.js'; import { getNestedError } from '@components/common/form/utils/getNestedError.js'; import { Field, FieldError, FieldLabel } from '@components/common/ui/Field.js'; import { InputGroupInput } from '@components/common/ui/InputGroup.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; import { useFormContext, RegisterOptions, FieldPath, FieldValues, Controller } from 'react-hook-form'; interface FileFieldProps<T extends FieldValues = FieldValues> extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'type'> { name: FieldPath<T>; label?: string; error?: string; helperText?: string; required?: boolean; validation?: RegisterOptions<T>; maxSize?: number; wrapperClassName?: string; } export function FileField<T extends FieldValues = FieldValues>({ name, label, error, wrapperClassName, helperText, required, validation, maxSize, className, accept, multiple = false, ...props }: FileFieldProps<T>) { const { control, formState: { errors }, watch } = useFormContext<T>(); const fieldError = getNestedError(name, errors, error); const fieldId = `field-${name}`; const files = watch(name); const formatFileSize = (bytes: number) => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; const { valueAsNumber, valueAsDate, ...cleanValidation } = validation || {}; const validationRules = { ...cleanValidation, ...(required && !validation?.required && { required: _('${field} is required', { field: label || name }) }), validate: { ...validation?.validate, fileSize: (fileList) => { if (!maxSize || !fileList || fileList.length === 0) return true; for (let i = 0; i < fileList.length; i++) { if (fileList[i].size > maxSize) { return _('File size must be less than ${maxSize}', { maxSize: formatFileSize(maxSize) }); } } return true; } } }; return ( <Field data-invalid={fieldError ? 'true' : 'false'} className={wrapperClassName} > {label && ( <FieldLabel htmlFor={fieldId}> <> {label} {required && <span className="text-destructive">*</span>} {helperText && <Tooltip content={helperText} position="top" />} </> </FieldLabel> )} <Controller name={name} control={control} rules={validationRules} render={({ field: { onChange, value, ...field } }) => ( <InputGroupInput {...field} id={fieldId} type="file" accept={accept} multiple={multiple} className={className} aria-invalid={fieldError !== undefined ? 'true' : 'false'} aria-describedby={ fieldError !== undefined ? `${fieldId}-error` : undefined } onChange={(e) => { onChange(e.target.files); }} {...props} /> )} /> {maxSize && ( <p className="file-size-hint"> Maximum file size: {formatFileSize(maxSize)} </p> )} {files && files.length > 0 && ( <div className="file-list"> <p className="file-list-label">Selected files:</p> <ul className="file-items"> {Array.from(files as FileList).map((file: File, index) => ( <li key={index}> {file.name} ({formatFileSize(file.size)}) </li> ))} </ul> </div> )} {fieldError && <FieldError>{fieldError}</FieldError>} </Field> ); } ================================================ FILE: packages/evershop/src/components/common/form/Form.tsx ================================================ import { Button } from '@components/common/ui/Button.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React, { useEffect, useState } from 'react'; import { useForm, FormProvider, UseFormProps, FieldValues, SubmitHandler, UseFormReturn } from 'react-hook-form'; import { toast } from 'react-toastify'; interface FormProps<T extends FieldValues = FieldValues> extends Omit< React.FormHTMLAttributes<HTMLFormElement>, 'onSubmit' | 'onError' > { form?: UseFormReturn<T>; action?: string; method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; formOptions?: UseFormProps<T>; onSubmit?: SubmitHandler<T>; onSuccess?: (response: any, data: T) => void; onError?: (error: string, data: T) => void; successMessage?: string; errorMessage?: string; submitBtn?: boolean; submitBtnText?: string; loading?: boolean; children: React.ReactNode; } export function Form<T extends FieldValues = FieldValues>({ form: externalForm, action, method = 'POST', formOptions, onSubmit, onSuccess, onError, successMessage = _('Saved successfully!'), errorMessage = _('Something went wrong! Please try again.'), submitBtn = true, submitBtnText = _('Save'), loading = false, children, className, noValidate = true, ...props }: FormProps<T>) { const theForm = externalForm || useForm<T>({ shouldUnregister: true, shouldFocusError: false, ...formOptions }); const { handleSubmit, formState: { isSubmitting } } = theForm; const defaultSubmit: SubmitHandler<T> = async (data) => { if (!action) { return; } try { const response = await fetch(action, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await response.json(); if (result.error) { if (onError) { onError(result.error.message, data); } else { toast.error(result.error.message || errorMessage); } } else if (onSuccess) { onSuccess(result, data); } else { toast.success(successMessage); } } catch (error) { if (onError) { onError( errorMessage || (error instanceof Error ? error.message : ''), data ); } else { toast.error( errorMessage || (error instanceof Error ? error.message : '') ); } } }; const [canFocus, setCanFocus] = useState(true); const onValidationError = () => { setCanFocus(true); }; useEffect(() => { if (theForm.formState.errors && canFocus) { const elements = Array.from( document.querySelectorAll('[aria-invalid="true"]') ) as HTMLElement[]; elements.sort( (a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top ); if (elements.length > 0) { const errorElement = elements[0]; errorElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); errorElement.focus({ preventScroll: true }); setCanFocus(false); } } }, [theForm.formState, canFocus]); const handleFormSubmit = onSubmit || defaultSubmit; return ( <FormProvider {...theForm}> <form onSubmit={handleSubmit(handleFormSubmit, onValidationError)} className={className} noValidate={noValidate} {...props} > <fieldset disabled={loading}>{children}</fieldset> {submitBtn && ( <div className="mt-4"> <Button title={submitBtnText} type="submit" onClick={() => { handleSubmit(handleFormSubmit, onValidationError)(); }} isLoading={isSubmitting || loading} > {submitBtnText} </Button> </div> )} </form> </FormProvider> ); } export { useFormContext } from 'react-hook-form'; export { Controller } from 'react-hook-form'; export type { Control, FieldPath, FieldValues } from 'react-hook-form'; ================================================ FILE: packages/evershop/src/components/common/form/InputField.tsx ================================================ import { Tooltip } from '@components/common/form/Tooltip.js'; import { getNestedError } from '@components/common/form/utils/getNestedError.js'; import { Field, FieldError, FieldLabel } from '@components/common/ui/Field.js'; import { InputGroup, InputGroupAddon, InputGroupInput } from '@components/common/ui/InputGroup.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; import { useFormContext, RegisterOptions, FieldPath, FieldValues, Controller } from 'react-hook-form'; interface InputFieldProps<T extends FieldValues = FieldValues> extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name'> { name: FieldPath<T>; label?: string; error?: string; helperText?: string; required?: boolean; validation?: RegisterOptions<T>; wrapperClassName?: string; prefixIcon?: React.ReactNode; suffixIcon?: React.ReactNode; } export function InputField<T extends FieldValues = FieldValues>({ name, label, error, helperText, required, validation, wrapperClassName, className, type = 'text', prefixIcon, suffixIcon, defaultValue, ...props }: InputFieldProps<T>) { const { control, formState: { errors } } = useFormContext<T>(); const fieldError = getNestedError(name, errors, error); const fieldId = `field-${name}`; const validationRules = { ...validation, ...(required && !validation?.required && { required: _('${field} is required', { field: label || name }) }) }; const renderInput = () => ( <Controller name={name} control={control} defaultValue={(defaultValue ?? '') as any} rules={validationRules} render={({ field }) => ( <InputGroupInput {...field} id={fieldId} type={type} aria-invalid={fieldError !== undefined ? 'true' : 'false'} aria-describedby={ fieldError !== undefined ? `${fieldId}-error` : undefined } {...props} /> )} /> ); // Special case: hidden inputs don't need labels or error messages if (type === 'hidden') { return ( <div> {renderInput()} {fieldError && <FieldError>{fieldError}</FieldError>} </div> ); } return ( <Field data-invalid={fieldError ? 'true' : 'false'} className={wrapperClassName} > {label && ( <FieldLabel htmlFor={fieldId}> <> {label} {required && <span className="text-destructive">*</span>} {helperText && <Tooltip content={helperText} position="top" />} </> </FieldLabel> )} <InputGroup> {renderInput()} {prefixIcon && ( <InputGroupAddon align={'inline-start'}>{prefixIcon}</InputGroupAddon> )} {suffixIcon && ( <InputGroupAddon align={'inline-end'}>{suffixIcon}</InputGroupAddon> )} </InputGroup> {fieldError && <FieldError>{fieldError}</FieldError>} </Field> ); } ================================================ FILE: packages/evershop/src/components/common/form/NumberField.tsx ================================================ import { Tooltip } from '@components/common/form/Tooltip.js'; import { getNestedError } from '@components/common/form/utils/getNestedError.js'; import { Field, FieldError, FieldLabel } from '@components/common/ui/Field.js'; import { InputGroup, InputGroupAddon, InputGroupInput } from '@components/common/ui/InputGroup.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; import { useFormContext, RegisterOptions, Controller } from 'react-hook-form'; interface NumberFieldProps { name: string; label?: string; placeholder?: string; className?: string; required?: boolean; disabled?: boolean; min?: number; max?: number; step?: number; allowDecimals?: boolean; unit?: string; unitPosition?: 'left' | 'right'; defaultValue?: number; error?: string; helperText?: string; validation?: RegisterOptions; onChange?: (value: number | null) => void; wrapperClassName?: string; prefixIcon?: React.ReactNode; suffixIcon?: React.ReactNode; } export function NumberField({ name, label, placeholder, className = '', wrapperClassName, required = false, disabled = false, min, max, step, allowDecimals = true, unit, unitPosition = 'right', defaultValue, error, helperText, validation, onChange, prefixIcon, suffixIcon, ...props }: NumberFieldProps) { const { control, formState: { errors } } = useFormContext(); const fieldError = getNestedError(name, errors, error); const fieldId = `field-${name}`; const validationRules: RegisterOptions = { setValueAs: (value) => { // Handle empty or null values if (value === '' || value === null || value === undefined) { return null; } // Convert string to number const numValue = allowDecimals ? parseFloat(value) : parseInt(value, 10); // Return null if conversion resulted in NaN return isNaN(numValue) ? null : numValue; } }; if (validation) { Object.assign(validationRules, validation); } if (required && !validationRules.required) { validationRules.required = _('${field} is required', { field: label || 'This field' }); } if (min !== undefined && !validationRules.min) { validationRules.min = { value: min, message: _('Value must be at least ${min}', { min: min.toString() }) }; } if (max !== undefined && !validationRules.max) { validationRules.max = { value: max, message: _('Value must be at most ${max}', { max: max.toString() }) }; } if (!allowDecimals && !validation?.validate) { validationRules.validate = (value) => { if (value === null || value === undefined || value === '') return true; return ( Number.isInteger(Number(value)) || _('Value must be a whole number') ); }; } else if ( !allowDecimals && validation?.validate && typeof validation.validate === 'object' ) { validationRules.validate = { ...validation.validate, isInteger: (value) => { if (value === null || value === undefined || value === '') return true; return ( Number.isInteger(Number(value)) || _('Value must be a whole number') ); } }; } const inputStep = step !== undefined ? step : allowDecimals ? 'any' : '1'; const inputClassName = `${fieldError ? 'error' : ''} ${ unit ? 'has-unit' : '' } ${className || ''} ${prefixIcon ? '!pl-10' : ''} ${ suffixIcon ? '!pr-10' : '' }`.trim(); const renderInput = () => ( <Controller name={name} control={control} defaultValue={defaultValue ?? null} rules={validationRules} render={({ field }) => ( <InputGroupInput {...field} id={fieldId} type="number" placeholder={placeholder} disabled={disabled} min={min} max={max} step={inputStep} className={inputClassName} aria-invalid={fieldError ? 'true' : 'false'} aria-describedby={fieldError ? `${fieldId}-error` : undefined} value={field.value ?? ''} onChange={(e) => { const inputValue = e.target.value; let numValue: number | null = null; if (inputValue !== '') { if (allowDecimals) { numValue = parseFloat(inputValue); } else { numValue = parseInt(inputValue, 10); } numValue = isNaN(numValue) ? null : numValue; } field.onChange(numValue); if (onChange) { onChange(numValue); } }} {...props} /> )} /> ); return ( <Field data-invalid={fieldError ? 'true' : 'false'} className={wrapperClassName} > {label && ( <FieldLabel htmlFor={fieldId}> <> {label} {required && <span className="text-destructive">*</span>} {helperText && <Tooltip content={helperText} position="top" />} </> </FieldLabel> )} <InputGroup> {renderInput()} {prefixIcon && ( <InputGroupAddon align={'inline-start'}>{prefixIcon}</InputGroupAddon> )} {suffixIcon && ( <InputGroupAddon align={'inline-end'}>{suffixIcon}</InputGroupAddon> )} {unit && ( <InputGroupAddon align={unitPosition === 'right' ? 'inline-end' : 'inline-start'} > {unit} </InputGroupAddon> )} </InputGroup> {fieldError && <FieldError>{fieldError}</FieldError>} </Field> ); } ================================================ FILE: packages/evershop/src/components/common/form/PasswordField.tsx ================================================ import { Tooltip } from '@components/common/form/Tooltip.js'; import { getNestedError } from '@components/common/form/utils/getNestedError.js'; import { Field, FieldError, FieldLabel } from '@components/common/ui/Field.js'; import { InputGroup, InputGroupAddon, InputGroupInput } from '@components/common/ui/InputGroup.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import { Eye, EyeClosed } from 'lucide-react'; import React from 'react'; import { useFormContext, RegisterOptions, FieldPath, FieldValues, Controller } from 'react-hook-form'; interface PasswordFieldProps<T extends FieldValues = FieldValues> extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'type'> { name: FieldPath<T>; label?: string; error?: string; helperText?: string; required?: boolean; minLength?: number; showToggle?: boolean; validation?: RegisterOptions<T>; wrapperClassName?: string; prefixIcon?: React.ReactNode; suffixIcon?: React.ReactNode; } export function PasswordField<T extends FieldValues = FieldValues>({ name, label, error, helperText, required, minLength = 6, showToggle = false, validation, wrapperClassName, className, defaultValue, prefixIcon, suffixIcon, ...props }: PasswordFieldProps<T>) { const { control, formState: { errors } } = useFormContext<T>(); const fieldError = getNestedError(name, errors, error); const fieldId = `field-${name}`; const [showPassword, setShowPassword] = React.useState(false); const validationRules = { ...validation, ...(required && !validation?.required && { required: _('${field} is required', { field: label || name }) }), minLength: validation?.minLength || { value: minLength, message: _('Password must be at least ${minLength} characters long', { minLength: minLength.toString() }) } }; const renderToggleButton = () => showToggle ? ( <button type="button" className="transition-colors" onClick={() => setShowPassword(!showPassword)} tabIndex={-1} > {showPassword ? ( <Eye className="h-5 w-5" /> ) : ( <EyeClosed className="h-5 w-5" /> )} </button> ) : null; const renderInput = () => ( <Controller name={name} control={control} defaultValue={defaultValue as any} rules={validationRules} render={({ field }) => ( <InputGroupInput {...field} value={field.value ?? ''} id={fieldId} type={showToggle && showPassword ? 'text' : 'password'} aria-invalid={fieldError !== undefined ? 'true' : 'false'} aria-describedby={ fieldError !== undefined ? `${fieldId}-error` : undefined } {...props} /> )} /> ); return ( <Field data-invalid={fieldError ? 'true' : 'false'} className={wrapperClassName} > {label && ( <FieldLabel htmlFor={fieldId}> <> {label} {required && <span className="text-destructive">*</span>} {helperText && <Tooltip content={helperText} position="top" />} </> </FieldLabel> )} <InputGroup> {renderInput()} {prefixIcon && ( <InputGroupAddon align={'inline-start'}>{prefixIcon}</InputGroupAddon> )} {(suffixIcon || showToggle) && ( <InputGroupAddon align={'inline-end'}> {suffixIcon || renderToggleButton()} </InputGroupAddon> )} </InputGroup> {fieldError && <FieldError>{fieldError}</FieldError>} </Field> ); } ================================================ FILE: packages/evershop/src/components/common/form/RadioGroupField.tsx ================================================ import { Tooltip } from '@components/common/form/Tooltip.js'; import { getNestedError } from '@components/common/form/utils/getNestedError.js'; import { Field, FieldError, FieldLabel, FieldLegend } from '@components/common/ui/Field.js'; import { Label } from '@components/common/ui/Label.js'; import { RadioGroup, RadioGroupItem } from '@components/common/ui/RadioGroup.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; import { useFormContext, RegisterOptions, FieldPath, FieldValues, Controller } from 'react-hook-form'; interface RadioOption { value: string | number; label: string; disabled?: boolean; } interface RadioGroupFieldProps<T extends FieldValues = FieldValues> extends Omit< React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'type' | 'value' | 'checked' | 'onChange' | 'onBlur' > { name: FieldPath<T>; options: RadioOption[]; label?: string; error?: string; helperText?: string; required?: boolean; disabled?: boolean; validation?: RegisterOptions<T>; defaultValue?: string | number; wrapperClassName?: string; } export function RadioGroupField<T extends FieldValues = FieldValues>({ name, options, label, error, wrapperClassName, helperText, className = '', required = false, disabled = false, validation, defaultValue, ...props }: RadioGroupFieldProps<T>) { const { control, formState: { errors } } = useFormContext<T>(); const fieldError = getNestedError(name, errors, error); const fieldId = `field-${name}`; const validationRules = { ...validation, ...(required && !validation?.required && { required: _('${field} is required', { field: label || name }) }) }; return ( <Field data-invalid={fieldError ? 'true' : 'false'} className={wrapperClassName} > {label && ( <FieldLabel> <> {label} {required && <span className="text-destructive">*</span>} {helperText && <Tooltip content={helperText} position="top" />} </> </FieldLabel> )} <Controller name={name} control={control} rules={validationRules} defaultValue={defaultValue as any} render={({ field }) => ( <RadioGroup value={String(field.value ?? '')} onValueChange={(value) => { const option = options.find((o) => String(o.value) === value); if (option) { field.onChange(option.value); } }} className={className} aria-invalid={fieldError !== undefined ? 'true' : 'false'} aria-describedby={ fieldError !== undefined ? `${fieldId}-error` : undefined } > {options.map((option) => ( <div key={option.value} className="flex items-center gap-2"> <RadioGroupItem value={String(option.value)} id={`${fieldId}-${option.value}`} disabled={disabled || option.disabled} /> <FieldLabel htmlFor={`${fieldId}-${option.value}`} className={`text-sm font-normal cursor-pointer ${ option.disabled ? 'opacity-50 cursor-not-allowed' : '' }`} > {option.label} </FieldLabel> </div> ))} </RadioGroup> )} /> {fieldError && <FieldError>{fieldError}</FieldError>} </Field> ); } ================================================ FILE: packages/evershop/src/components/common/form/RangeField.tsx ================================================ import { Tooltip } from '@components/common/form/Tooltip.js'; import { getNestedError } from '@components/common/form/utils/getNestedError.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; import { useFormContext, RegisterOptions, FieldPath, FieldValues } from 'react-hook-form'; interface RangeFieldProps<T extends FieldValues = FieldValues> extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'type'> { name: FieldPath<T>; label?: string; error?: string; helperText?: string; required?: boolean; validation?: RegisterOptions<T>; showValue?: boolean; defaultValue?: number; wrapperClassName?: string; } export function RangeField<T extends FieldValues = FieldValues>({ name, label, error, wrapperClassName, helperText, required, validation, showValue = true, defaultValue, className, min = 0, max = 100, step = 1, ...props }: RangeFieldProps<T>) { const { register, formState: { errors }, watch } = useFormContext<T>(); const fieldError = getNestedError(name, errors, error); const fieldId = `field-${name}`; const value = watch(name) || min; const { valueAsDate, pattern, ...cleanValidation } = validation || {}; const validationRules = { ...cleanValidation, ...(required && { required: _('${field} is required', { field: label || name }) }), valueAsNumber: true } as const; return ( <div className={`form-field ${wrapperClassName} ${fieldError ? 'error' : ''}`} > {label && ( <label htmlFor={fieldId}> {label} {required && <span className="text-destructive">*</span>} {showValue && <span className="range-value">({value})</span>} {helperText && <Tooltip content={helperText} position="top" />} </label> )} <input id={fieldId} type="range" min={min} max={max} step={step} {...register(name, validationRules)} className={className} aria-invalid={fieldError !== undefined ? 'true' : 'false'} aria-describedby={ fieldError !== undefined ? `${fieldId}-error` : undefined } {...props} /> <div className="range-labels"> <span>{min}</span> <span>{max}</span> </div> {fieldError && ( <p id={`${fieldId}-error`} className="field-error"> {fieldError} </p> )} </div> ); } ================================================ FILE: packages/evershop/src/components/common/form/ReactSelectCreatableField.tsx ================================================ import { getNestedError } from '@components/common/form/utils/getNestedError.js'; import { Field, FieldLabel } from '@components/common/ui/Field.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; import { Controller, FieldPath, FieldValues, RegisterOptions, useFormContext } from 'react-hook-form'; import CreatableSelect, { CreatableProps } from 'react-select/creatable'; interface SelectOption { value: any; label: string; [key: string]: unknown; } interface ReactSelectCreatableFieldProps<T extends FieldValues = FieldValues> extends Omit< CreatableProps<SelectOption, boolean, any>, 'name' | 'value' | 'onChange' > { name: FieldPath<T>; label?: string; error?: string; helperText?: string; required?: boolean; validation?: RegisterOptions<T>; options: SelectOption[]; className?: string; wrapperClassName?: string; defaultValue?: any; onCreateOption?: (inputValue: string) => void; formatCreateLabel?: (inputValue: string) => string; } export function ReactSelectCreatableField<T extends FieldValues = FieldValues>({ name, label, error, wrapperClassName = 'form-field', helperText, required, validation, options, className, isMulti = false, defaultValue, onCreateOption, formatCreateLabel = (inputValue: string) => `Create "${inputValue}"`, ...selectProps }: ReactSelectCreatableFieldProps<T>) { const { control, unregister, formState: { errors } } = useFormContext<T>(); const fieldId = `field-${name}`; const [dynamicOptions, setDynamicOptions] = React.useState<SelectOption[]>(options); React.useEffect(() => { setDynamicOptions(options); }, [options]); React.useEffect(() => { return () => { unregister(name); }; }, [name, unregister]); const validationRules = { ...validation, ...(required && { required: _('${field} is required', { field: label || name }) }) }; const fieldError = getNestedError(name, errors, error); return ( <Controller name={name} control={control} rules={validationRules} defaultValue={defaultValue} render={({ field, fieldState }) => { const handleCreateOption = (inputValue: string) => { const newOption = { value: inputValue.toLowerCase().replace(/\W/g, ''), label: inputValue }; const optionExists = dynamicOptions.some( (option) => option.value === newOption.value || option.label === newOption.label ); if (!optionExists) { setDynamicOptions((prev) => { const updated = [...prev, newOption]; return updated; }); } if (onCreateOption) { onCreateOption(inputValue); } if (isMulti) { const currentValues = (field.value as any[]) || []; if (!currentValues.includes(newOption.value)) { const newValues = [...currentValues, newOption.value]; field.onChange(newValues); } } else { field.onChange(newOption.value); } }; return ( <Field data-invalid={fieldError ? 'true' : 'false'} className={wrapperClassName} > {label && ( <FieldLabel htmlFor={fieldId}> {label} {required && <span className="text-destructive">*</span>} </FieldLabel> )} <CreatableSelect {...field} {...selectProps} inputId={fieldId} options={dynamicOptions} isMulti={isMulti} formatCreateLabel={formatCreateLabel} onCreateOption={handleCreateOption} value={ isMulti ? dynamicOptions.filter((option) => field.value?.includes(option.value) ) : dynamicOptions.find( (option) => option.value === field.value ) || null } onChange={(selectedOption) => { if (isMulti) { const values = selectedOption ? (selectedOption as SelectOption[]).map( (option) => option.value ) : []; field.onChange(values); } else { field.onChange( selectedOption ? (selectedOption as SelectOption).value : null ); } }} classNamePrefix="react-select" styles={{ control: (base, state) => ({ ...base, minHeight: 'auto', border: '1px solid #d1d5db', borderRadius: '0.375rem', boxShadow: 'none', transition: 'border-color 0.15s ease-in-out', '&:hover': { borderColor: '#d1d5db' }, ...(state.isFocused && { borderColor: '#3b82f6', boxShadow: '0 0 0 1px rgb(59, 130, 246)' }) }), input: (base) => ({ ...base, '& input': { boxShadow: 'none !important', outline: 'none !important' } }) }} /> {fieldError && ( <p id={`${fieldId}-error`} className="field-error"> {fieldError} </p> )} {helperText && !fieldError && ( <p id={`${fieldId}-helper`} className="field-helper"> {helperText} </p> )} </Field> ); }} /> ); } ================================================ FILE: packages/evershop/src/components/common/form/ReactSelectField.tsx ================================================ import { Tooltip } from '@components/common/form/Tooltip.js'; import { getNestedError } from '@components/common/form/utils/getNestedError.js'; import { Field, FieldError, FieldLabel } from '@components/common/ui/Field.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import { cn } from '@evershop/evershop/lib/util/cn'; import React from 'react'; import { Controller, FieldPath, FieldValues, RegisterOptions, useFormContext } from 'react-hook-form'; import Select, { Props as ReactSelectProps } from 'react-select'; interface SelectOption { value: any; label: string; [key: string]: any; } interface ReactSelectFieldProps<T extends FieldValues = FieldValues> extends Omit<ReactSelectProps<SelectOption>, 'name' | 'value' | 'onChange'> { name: FieldPath<T>; label?: string; error?: string; helperText?: string; required?: boolean; validation?: RegisterOptions<T>; options: SelectOption[]; className?: string; wrapperClassName?: string; defaultValue?: any; } export function ReactSelectField<T extends FieldValues = FieldValues>({ name, label, error, wrapperClassName = 'form-field', helperText, required, validation, options, className, isMulti = false, defaultValue, ...selectProps }: ReactSelectFieldProps<T>) { const { control, formState: { errors } } = useFormContext<T>(); const fieldError = getNestedError(name, errors, error); const fieldId = `field-${name}`; const validationRules = { ...validation, ...(required && !validation?.required && { required: _('${field} is required', { field: label || name }) }) }; return ( <Field data-invalid={fieldError ? 'true' : 'false'} className={wrapperClassName} id={`field-${name}`} > {label && ( <FieldLabel htmlFor={fieldId}> <> {label} {required && <span className="text-destructive">*</span>} {helperText && <Tooltip content={helperText} position="top" />} </> </FieldLabel> )} <Controller name={name} control={control} rules={validationRules} defaultValue={defaultValue} render={({ field }) => ( <Select {...field} {...selectProps} inputId={fieldId} options={options} isMulti={isMulti} className={cn(className)} value={ isMulti ? options.filter((option) => (field.value || defaultValue || [])?.includes(option.value) ) : options.find( (option) => option.value === (field.value ?? defaultValue) ) || null } onChange={(selectedOption) => { if (isMulti) { const values = selectedOption ? (selectedOption as SelectOption[]).map( (option) => option.value ) : []; field.onChange(values); } else { field.onChange( selectedOption ? (selectedOption as SelectOption).value : null ); } }} classNamePrefix="react-select" aria-invalid={fieldError !== undefined ? 'true' : 'false'} aria-describedby={ fieldError !== undefined ? `${fieldId}-error` : undefined } classNames={{ control: (state) => cn( 'min-h-auto border border-input rounded-md shadow-xs transition-[color,box-shadow]', state.isFocused && 'border-ring ring-[3px] ring-ring/50', fieldError && 'border-destructive ring-[3px] ring-destructive/20' ), input: () => 'outline-none shadow-none', menu: () => 'bg-popover border border-input rounded-md shadow-md', option: (state) => cn( 'px-3 py-2 cursor-pointer', state.isSelected && 'bg-primary text-primary-foreground', state.isFocused && !state.isSelected && 'bg-accent' ) }} /> )} /> {fieldError && <FieldError>{fieldError}</FieldError>} </Field> ); } ================================================ FILE: packages/evershop/src/components/common/form/SelectField.tsx ================================================ import { Tooltip } from '@components/common/form/Tooltip.js'; import { getNestedError } from '@components/common/form/utils/getNestedError.js'; import { Field, FieldError, FieldLabel } from '@components/common/ui/Field.js'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@components/common/ui/Select.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; import { useFormContext, RegisterOptions, FieldPath, FieldValues, Controller } from 'react-hook-form'; interface SelectOption { value: string | number; label: string; disabled?: boolean; } interface SelectFieldProps<T extends FieldValues = FieldValues> { name: FieldPath<T>; label?: string; error?: string; helperText?: string; required?: boolean; validation?: RegisterOptions<T>; options: SelectOption[]; placeholder?: string; wrapperClassName?: string; className?: string; disabled?: boolean; defaultValue?: string | number; id?: string; onChange?: (value: string | number) => void; } export function SelectField<T extends FieldValues = FieldValues>({ name, label, error, helperText, required, validation, options, placeholder, wrapperClassName, className, defaultValue, disabled, id, onChange: onChangeCallback }: SelectFieldProps<T>) { const { control, formState: { errors } } = useFormContext<T>(); const fieldError = getNestedError(name, errors, error); const fieldId = id || `field-${name}`; const hasDefaultValue = defaultValue !== undefined && defaultValue !== null && defaultValue !== ''; const validationRules = { ...validation, ...(required && !validation?.required && { required: { value: true, message: _('${field} is required', { field: label || name }) }, validate: { ...validation?.validate, notEmpty: (value) => { if ( required && (value === '' || value === null || value === undefined) ) { return _('${field} is required', { field: label || name }); } return true; } } }) }; return ( <Field data-invalid={fieldError ? 'true' : 'false'} className={wrapperClassName} > {label && ( <FieldLabel htmlFor={fieldId}> <> {label} {required && <span className="text-destructive">*</span>} {helperText && <Tooltip content={helperText} position="top" />} </> </FieldLabel> )} <Controller name={name} control={control} rules={validationRules} defaultValue={hasDefaultValue ? defaultValue : ('' as any)} render={({ field }) => ( <Select value={options.find((o) => o.value === field.value)} onValueChange={(value) => { const newValue = value?.value === '' ? '' : value?.value; field.onChange(newValue); if (onChangeCallback && value !== null) { onChangeCallback(value.value); } }} disabled={disabled} > <SelectTrigger id={fieldId} className={className} aria-invalid={fieldError !== undefined ? 'true' : 'false'} aria-describedby={ fieldError !== undefined ? `${fieldId}-error` : undefined } > <SelectValue> {options.find((o) => String(o.value) === String(field.value)) ?.label || placeholder} </SelectValue> </SelectTrigger> <SelectContent> {placeholder && ( <SelectItem value="" disabled> {placeholder} </SelectItem> )} {options.map((option) => ( <SelectItem key={option.value} value={option} disabled={option.disabled} > {option.label} </SelectItem> ))} </SelectContent> </Select> )} /> {fieldError && <FieldError>{fieldError}</FieldError>} </Field> ); } ================================================ FILE: packages/evershop/src/components/common/form/TelField.tsx ================================================ import { Tooltip } from '@components/common/form/Tooltip.js'; import { getNestedError } from '@components/common/form/utils/getNestedError.js'; import { Field, FieldError, FieldLabel } from '@components/common/ui/Field.js'; import { InputGroup, InputGroupAddon, InputGroupInput } from '@components/common/ui/InputGroup.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; import { useFormContext, RegisterOptions, FieldPath, FieldValues, Controller } from 'react-hook-form'; interface TelFieldProps<T extends FieldValues = FieldValues> extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'type'> { name: FieldPath<T>; label?: string; error?: string; helperText?: string; required?: boolean; validation?: RegisterOptions<T>; wrapperClassName?: string; prefixIcon?: React.ReactNode; suffixIcon?: React.ReactNode; } export function TelField<T extends FieldValues = FieldValues>({ name, label, error, wrapperClassName, helperText, required, validation, className, defaultValue, prefixIcon, suffixIcon, ...props }: TelFieldProps<T>) { const { control, formState: { errors } } = useFormContext<T>(); const fieldError = getNestedError(name, errors, error); const fieldId = `field-${name}`; const validationRules = { ...validation, ...(required && !validation?.required && { required: _('${field} is required', { field: label || name }) }) }; const inputClassName = `${fieldError !== undefined ? 'error' : ''} ${ className || '' } ${prefixIcon ? '!pl-10' : ''} ${suffixIcon ? '!pr-10' : ''}`.trim(); const renderInput = () => ( <Controller name={name} control={control} defaultValue={defaultValue as any} rules={validationRules} render={({ field }) => ( <InputGroupInput {...field} value={field.value ?? ''} id={fieldId} type="tel" className={inputClassName} aria-invalid={fieldError !== undefined ? 'true' : 'false'} aria-describedby={ fieldError !== undefined ? `${fieldId}-error` : undefined } {...props} /> )} /> ); return ( <Field data-invalid={fieldError ? 'true' : 'false'} className={wrapperClassName} > {label && ( <FieldLabel htmlFor={fieldId}> <> {label} {required && <span className="text-destructive">*</span>} {helperText && <Tooltip content={helperText} position="top" />} </> </FieldLabel> )} <InputGroup> {renderInput()} {prefixIcon && ( <InputGroupAddon align={'inline-start'}>{prefixIcon}</InputGroupAddon> )} {suffixIcon && ( <InputGroupAddon align={'inline-end'}>{suffixIcon}</InputGroupAddon> )} </InputGroup> {fieldError && <FieldError>{fieldError}</FieldError>} </Field> ); } ================================================ FILE: packages/evershop/src/components/common/form/TextareaField.tsx ================================================ import { Tooltip } from '@components/common/form/Tooltip.js'; import { getNestedError } from '@components/common/form/utils/getNestedError.js'; import { Field, FieldError, FieldLabel } from '@components/common/ui/Field.js'; import { Textarea } from '@components/common/ui/Textarea.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; import { useFormContext, RegisterOptions, FieldPath, FieldValues, Controller } from 'react-hook-form'; interface TextareaFieldProps<T extends FieldValues = FieldValues> extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'name'> { name: FieldPath<T>; label?: string; error?: string; helperText?: string; required?: boolean; validation?: RegisterOptions<T>; wrapperClassName?: string; } export function TextareaField<T extends FieldValues = FieldValues>({ name, label, error, helperText, wrapperClassName, required, validation, className, rows = 4, defaultValue, ...props }: TextareaFieldProps<T>) { const { control, formState: { errors } } = useFormContext<T>(); const fieldError = getNestedError(name, errors, error); const fieldId = `field-${name}`; const validationRules = { ...validation, ...(required && !validation?.required && { required: _('${field} is required', { field: label || name }) }) }; return ( <Field data-invalid={fieldError ? 'true' : 'false'} className={wrapperClassName} > {label && ( <FieldLabel htmlFor={fieldId}> <> {label} {required && <span className="text-destructive">*</span>} {helperText && <Tooltip content={helperText} position="top" />} </> </FieldLabel> )} <Controller name={name} control={control} rules={validationRules} defaultValue={defaultValue as any} render={({ field }) => ( <Textarea {...field} id={fieldId} rows={rows} className={`${fieldError !== undefined ? 'error' : ''} ${ className || '' }`} aria-invalid={fieldError !== undefined ? 'true' : 'false'} aria-describedby={ fieldError !== undefined ? `${fieldId}-error` : undefined } {...props} /> )} /> {fieldError && <FieldError>{fieldError}</FieldError>} </Field> ); } ================================================ FILE: packages/evershop/src/components/common/form/TimeField.tsx ================================================ import { Tooltip } from '@components/common/form/Tooltip.js'; import { getNestedError } from '@components/common/form/utils/getNestedError.js'; import { Field, FieldError, FieldLabel } from '@components/common/ui/Field.js'; import { InputGroup, InputGroupInput } from '@components/common/ui/InputGroup.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; import { useFormContext, RegisterOptions, FieldPath, FieldValues, Controller } from 'react-hook-form'; interface TimeFieldProps<T extends FieldValues = FieldValues> extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'type'> { name: FieldPath<T>; label?: string; error?: string; helperText?: string; required?: boolean; validation?: RegisterOptions<T>; wrapperClassName?: string; } export function TimeField<T extends FieldValues = FieldValues>({ name, label, error, wrapperClassName, helperText, required, validation, className, min, max, step, ...props }: TimeFieldProps<T>) { const { control, formState: { errors } } = useFormContext<T>(); const fieldError = getNestedError(name, errors, error); const fieldId = `field-${name}`; const { valueAsNumber, valueAsDate, ...cleanValidation } = validation || {}; const validationRules = { ...cleanValidation, ...(required && { required: _('${field} is required', { field: label || name }) }), validate: { ...validation?.validate, minTime: (value) => { if (!min || !value) return true; return ( value >= min || _('Time must be after ${min}', { min: min.toString() }) ); }, maxTime: (value) => { if (!max || !value) return true; return ( value <= max || _('Time must be before ${max}', { max: max.toString() }) ); } } }; return ( <Field data-invalid={fieldError ? 'true' : 'false'} className={wrapperClassName} > {label && ( <FieldLabel htmlFor={fieldId}> <> {label} {required && <span className="text-destructive">*</span>} {helperText && <Tooltip content={helperText} position="top" />} </> </FieldLabel> )} <Controller name={name} control={control} rules={validationRules} render={({ field }) => ( <InputGroup> <InputGroupInput {...field} value={field.value ?? ''} id={fieldId} type="time" min={min} max={max} step={step} className={className} aria-invalid={fieldError !== undefined ? 'true' : 'false'} aria-describedby={ fieldError !== undefined ? `${fieldId}-error` : undefined } {...props} /> </InputGroup> )} /> {fieldError && <FieldError>{fieldError}</FieldError>} </Field> ); } ================================================ FILE: packages/evershop/src/components/common/form/ToggleField.tsx ================================================ import { Tooltip } from '@components/common/form/Tooltip.js'; import { getNestedError } from '@components/common/form/utils/getNestedError.js'; import { Field, FieldError, FieldLabel } from '@components/common/ui/Field.js'; import { Switch } from '@components/common/ui/Switch.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; import { Controller, FieldPath, FieldValues, RegisterOptions, useFormContext } from 'react-hook-form'; interface ToggleFieldProps<T extends FieldValues = FieldValues> { name: FieldPath<T>; label?: string; error?: string; helperText?: string; required?: boolean; validation?: RegisterOptions<T>; wrapperClassName?: string; disabled?: boolean; defaultValue?: boolean | 0 | 1; trueValue?: boolean | 1; falseValue?: boolean | 0; trueLabel?: string; falseLabel?: string; size?: 'sm' | 'default'; onChange?: (value: boolean | 0 | 1) => void; } export function ToggleField<T extends FieldValues = FieldValues>({ name, label, error, helperText, required, validation, wrapperClassName, disabled = false, defaultValue = false, trueValue = true, falseValue = false, trueLabel = 'Yes', falseLabel = 'No', size = 'default', onChange }: ToggleFieldProps<T>) { const { control, formState: { errors } } = useFormContext<T>(); const fieldError = getNestedError(name, errors, error); const fieldId = `field-${name}`; const validationRules = { ...validation, ...(required && !validation?.required && { required: _('${field} is required', { field: label || name }) }) }; return ( <Field data-invalid={fieldError ? 'true' : 'false'} className={wrapperClassName} > {label && ( <FieldLabel htmlFor={fieldId}> {label} {required && <span className="text-destructive ml-1">*</span>} {helperText && <Tooltip content={helperText} position="top" />} </FieldLabel> )} <Controller name={name} control={control} rules={validationRules} defaultValue={defaultValue as any} render={({ field }) => { const isActive = field.value === trueValue; return ( <div className="flex items-center gap-3"> <Switch id={fieldId} size={size} checked={isActive} onCheckedChange={(checked) => { const newValue = checked ? trueValue : falseValue; field.onChange(newValue); onChange?.(newValue); }} disabled={disabled} aria-invalid={fieldError ? 'true' : 'false'} aria-describedby={fieldError ? `${fieldId}-error` : undefined} /> <span className="text-sm text-muted-foreground"> {isActive ? trueLabel : falseLabel} </span> </div> ); }} /> {fieldError && ( <FieldError id={`${fieldId}-error`}>{fieldError}</FieldError> )} </Field> ); } ================================================ FILE: packages/evershop/src/components/common/form/Tooltip.tsx ================================================ import React, { useState } from 'react'; interface TooltipProps { content: string; position?: 'top' | 'bottom' | 'left' | 'right'; className?: string; } export function Tooltip({ content, position = 'top', className = '' }: TooltipProps) { const [isVisible, setIsVisible] = useState(false); const positionClasses = { top: 'bottom-full left-1/2 transform -translate-x-1/2 mb-2', bottom: 'top-full left-1/2 transform -translate-x-1/2 mt-2', left: 'right-full top-1/2 transform -translate-y-1/2 mr-2', right: 'left-full top-1/2 transform -translate-y-1/2 ml-2' }; const arrowClasses = { top: 'top-full left-1/2 transform -translate-x-1/2 border-l-4 border-r-4 border-t-4 border-l-transparent border-r-transparent border-t-gray-800', bottom: 'bottom-full left-1/2 transform -translate-x-1/2 border-l-4 border-r-4 border-b-4 border-l-transparent border-r-transparent border-b-gray-800', left: 'left-full top-1/2 transform -translate-y-1/2 border-t-4 border-b-4 border-l-4 border-t-transparent border-b-transparent border-l-gray-800', right: 'right-full top-1/2 transform -translate-y-1/2 border-t-4 border-b-4 border-r-4 border-t-transparent border-b-transparent border-r-gray-800' }; return ( <div className={`relative inline-flex ${className}`}> <button type="button" className="inline-flex items-center justify-center w-4 h-4 ml-1 text-gray-400 hover:text-gray-600 transition-colors duration-200" onMouseEnter={() => setIsVisible(true)} onMouseLeave={() => setIsVisible(false)} onFocus={() => setIsVisible(true)} onBlur={() => setIsVisible(false)} tabIndex={-1} > <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"> <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" clipRule="evenodd" /> </svg> </button> {isVisible && ( <div className={`absolute z-50 px-3 py-2 text-sm font-normal text-white bg-gray-800 rounded-lg shadow-lg transition-all duration-300 ease-in-out transform ${positionClasses[position]} opacity-100 scale-100`} style={{ minWidth: '200px', maxWidth: '300px' }} > {content} <div className={`absolute w-0 h-0 ${arrowClasses[position]}`}></div> </div> )} </div> ); } ================================================ FILE: packages/evershop/src/components/common/form/UrlField.tsx ================================================ import { Tooltip } from '@components/common/form/Tooltip.js'; import { getNestedError } from '@components/common/form/utils/getNestedError.js'; import { Field, FieldError, FieldLabel } from '@components/common/ui/Field.js'; import { InputGroup, InputGroupAddon, InputGroupInput } from '@components/common/ui/InputGroup.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; import { useFormContext, RegisterOptions, FieldPath, FieldValues, Controller } from 'react-hook-form'; interface UrlFieldProps<T extends FieldValues = FieldValues> extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'type'> { name: FieldPath<T>; label?: string; error?: string; helperText?: string; required?: boolean; validation?: RegisterOptions<T>; defaultValue?: string; wrapperClassName?: string; prefixIcon?: React.ReactNode; suffixIcon?: React.ReactNode; } export function UrlField<T extends FieldValues = FieldValues>({ name, label, error, wrapperClassName, helperText, required, validation, defaultValue, className, prefixIcon, suffixIcon, ...props }: UrlFieldProps<T>) { const { control, formState: { errors } } = useFormContext<T>(); const fieldError = getNestedError(name, errors, error); const fieldId = `field-${name}`; const { valueAsNumber, valueAsDate, ...cleanValidation } = validation || {}; const validationRules = { ...cleanValidation, ...(required && { required: _('${field} is required', { field: label || name }) }), pattern: validation?.pattern || { value: /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/, message: _('Please enter a valid URL') } }; const inputClassName = `${fieldError !== undefined ? 'error' : ''} ${ className || '' } ${prefixIcon ? '!pl-10' : ''} ${suffixIcon ? '!pr-10' : ''}`.trim(); const renderInput = () => ( <Controller name={name} control={control} defaultValue={defaultValue as any} rules={validationRules} render={({ field }) => ( <InputGroupInput {...field} id={fieldId} type="url" className={inputClassName} aria-invalid={fieldError !== undefined ? 'true' : 'false'} aria-describedby={ fieldError !== undefined ? `${fieldId}-error` : undefined } {...props} /> )} /> ); return ( <Field data-invalid={fieldError ? 'true' : 'false'} className={wrapperClassName} > {label && ( <FieldLabel htmlFor={fieldId}> <> {label} {required && <span className="text-destructive">*</span>} {helperText && <Tooltip content={helperText} position="top" />} </> </FieldLabel> )} <InputGroup> {renderInput()} {prefixIcon && ( <InputGroupAddon align={'inline-start'}>{prefixIcon}</InputGroupAddon> )} {suffixIcon && ( <InputGroupAddon align={'inline-end'}>{suffixIcon}</InputGroupAddon> )} </InputGroup> {fieldError && <FieldError>{fieldError}</FieldError>} </Field> ); } ================================================ FILE: packages/evershop/src/components/common/form/editor/GetColumnClasses.tsx ================================================ const getColumnClasses = (size: number): string => { switch (size) { case 1: return 'md:col-span-1'; case 2: return 'md:col-span-2'; case 3: return 'md:col-span-3'; default: return 'md:col-span-1'; } }; export { getColumnClasses }; ================================================ FILE: packages/evershop/src/components/common/form/editor/GetRowClasses.tsx ================================================ const getRowClasses = (size: number): string => { switch (size) { case 1: return 'md:grid-cols-1'; case 2: return 'md:grid-cols-2'; case 3: return 'md:grid-cols-3'; case 4: return 'md:grid-cols-4'; case 5: return 'md:grid-cols-5'; default: return 'md:grid-cols-1'; } }; export { getRowClasses }; ================================================ FILE: packages/evershop/src/components/common/form/editor/RawToolWrapper.ts ================================================ /** * Wrapper for @editorjs/raw that fixes keyboard event handling issues * This ensures backspace and other keys work properly in the raw HTML block */ export class RawToolWrapper { private rawTool: any; private api: any; private data: any; private config: any; constructor({ data, config, api, block }: any) { this.data = data; this.config = config; this.api = api; // We'll load the actual Raw tool dynamically this.initializeRawTool({ data, config, api, block }); } async initializeRawTool(params: any) { const { default: RawTool } = await import('@editorjs/raw'); this.rawTool = new RawTool(params); } static get toolbox() { return { title: 'Raw HTML', icon: '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>' }; } render() { const wrapper = document.createElement('div'); wrapper.classList.add('ce-rawtool'); const textarea = document.createElement('textarea'); textarea.classList.add('ce-rawtool__textarea'); textarea.placeholder = 'Enter HTML code'; textarea.value = this.data?.html || ''; // Prevent EditorJS from handling keyboard events inside textarea textarea.addEventListener('keydown', (event) => { event.stopPropagation(); }); textarea.addEventListener('keyup', (event) => { event.stopPropagation(); }); textarea.addEventListener('paste', (event) => { event.stopPropagation(); }); // Handle input changes textarea.addEventListener('input', () => { this.data = { html: textarea.value }; }); wrapper.appendChild(textarea); return wrapper; } save(blockContent: HTMLElement) { const textarea = blockContent.querySelector('textarea'); return { html: textarea?.value || '' }; } static get sanitize() { return { html: true }; } static get isReadOnlySupported() { return true; } } ================================================ FILE: packages/evershop/src/components/common/form/editor/RowTemplates.tsx ================================================ import React from 'react'; import { v4 as uuidv4 } from 'uuid'; import { getColumnClasses } from './GetColumnClasses.js'; import { getRowClasses } from './GetRowClasses.js'; function RowTemplates({ addRow }: { addRow: (row: any) => void }) { const templates = { 1: () => ( <svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 48 48" aria-hidden="true" focusable="false" fill="#949494" > <path d="M0 10a2 2 0 0 1 2-2h44a2 2 0 0 1 2 2v28a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V10Z" /> </svg> ), '1:1': () => ( <svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 48 48" aria-hidden="true" focusable="false" fill="#949494" > <path d="M0 10a2 2 0 0 1 2-2h19a2 2 0 0 1 2 2v28a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V10Zm25 0a2 2 0 0 1 2-2h19a2 2 0 0 1 2 2v28a2 2 0 0 1-2 2H27a2 2 0 0 1-2-2V10Z" /> </svg> ), '1:2': () => ( <svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 48 48" aria-hidden="true" focusable="false" fill="#949494" > <path d="M0 10a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v28a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V10Zm17 0a2 2 0 0 1 2-2h27a2 2 0 0 1 2 2v28a2 2 0 0 1-2 2H19a2 2 0 0 1-2-2V10Z" /> </svg> ), '2:1': () => ( <svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 48 48" aria-hidden="true" focusable="false" fill="#949494" > <path d="M0 10a2 2 0 0 1 2-2h27a2 2 0 0 1 2 2v28a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V10Zm33 0a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v28a2 2 0 0 1-2 2H35a2 2 0 0 1-2-2V10Z" /> </svg> ), '2:3': () => ( <svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 48 48" aria-hidden="true" focusable="false" fill="#949494" > <rect x="0" y="8" width="18.4" height="32" rx="2" ry="2" /> <rect x="21.6" y="8" width="24" height="32" rx="2" ry="2" /> </svg> ), '3:2': () => ( <svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 48 48" aria-hidden="true" focusable="false" fill="#949494" > <rect x="0" y="8" width="24" height="32" rx="2" ry="2" /> <rect x="27.2" y="8" width="18.4" height="32" rx="2" ry="2" /> </svg> ), '1:1:1': () => ( <svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 48 48" aria-hidden="true" focusable="false" fill="#949494" > <path d="M0 10a2 2 0 0 1 2-2h10.531c1.105 0 1.969.895 1.969 2v28c0 1.105-.864 2-1.969 2H2a2 2 0 0 1-2-2V10Zm16.5 0c0-1.105.864-2 1.969-2H29.53c1.105 0 1.969.895 1.969 2v28c0 1.105-.864 2-1.969 2H18.47c-1.105 0-1.969-.895-1.969-2V10Zm17 0c0-1.105.864-2 1.969-2H46a2 2 0 0 1 2 2v28a2 2 0 0 1-2 2H35.469c-1.105 0-1.969-.895-1.969-2V10Z" /> </svg> ), '1:2:1': () => ( <svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 48 48" aria-hidden="true" focusable="false" fill="#949494" > <path d="M0 10a2 2 0 0 1 2-2h7.531c1.105 0 1.969.895 1.969 2v28c0 1.105-.864 2-1.969 2H2a2 2 0 0 1-2-2V10Zm13.5 0c0-1.105.864-2 1.969-2H32.53c1.105 0 1.969.895 1.969 2v28c0 1.105-.864 2-1.969 2H15.47c-1.105 0-1.969-.895-1.969-2V10Zm23 0c0-1.105.864-2 1.969-2H46a2 2 0 0 1 2 2v28a2 2 0 0 1-2 2h-7.531c-1.105 0-1.969-.895-1.969-2V10Z" /> </svg> ) }; return ( <div className="row-templates flex justify-center gap-7 px-3"> {Object.keys(templates).map((key) => ( <a key={key} href="#" onClick={(e) => { e.preventDefault(); const split = key.split(':').map((val) => parseInt(val, 10)); const sum = split.reduce((acc, val) => acc + val, 0); const rowClassName = getRowClasses(sum); const columns = split.map((size) => { const columnClassName = getColumnClasses(size); return { size, className: columnClassName, id: `c__${uuidv4()}` }; }); addRow({ id: `r__${uuidv4()}`, editSetting: true, columns, size: sum, className: rowClassName }); }} > {templates[key]()} </a> ))} </div> ); } export { RowTemplates }; ================================================ FILE: packages/evershop/src/components/common/form/utils/getNestedError.ts ================================================ /** * Helper function to get nested error from react-hook-form errors object * Handles both simple field names and nested array field names using dot notation (e.g., "attributes.0.value") * Also supports legacy bracket notation for backward compatibility */ export const getNestedError = ( name: string, errors: any, error?: string ): string | undefined => { if (error) return error; if (!name.includes('.') && !name.includes('[')) { return errors[name]?.message; } let parts: string[]; if (name.includes('[')) { parts = name.split(/[\[\]]+/).filter(Boolean); } else { parts = name.split('.'); } let current = errors; for (const part of parts) { if (current === null || current === undefined) return undefined; const index = parseInt(part); if (!isNaN(index)) { current = current[index]; } else { current = current[part]; } } return current?.message; }; ================================================ FILE: packages/evershop/src/components/common/index.tsx ================================================ import Area from './Area.jsx'; import { HydrateAdmin } from './react/client/HydrateAdmin.jsx'; import { HydrateFrontStore } from './react/client/HydrateFrontStore.jsx'; import { renderHtml } from './react/server/render.jsx'; export { Area }; export { HydrateFrontStore }; export { HydrateAdmin }; export { renderHtml }; export default Area; ================================================ FILE: packages/evershop/src/components/common/locale/CountryOption.jsx ================================================ import PropTypes from 'prop-types'; import React from 'react'; function CountryOptions(props) { const { countries, children } = props; const options = [ { value: 'AF', text: 'Afghanistan' }, { value: 'AL', text: 'Albania' }, { value: 'DZ', text: 'Algeria' }, { value: 'AS', text: 'American Samoa' }, { value: 'AD', text: 'Andorra' }, { value: 'AO', text: 'Angola' }, { value: 'AI', text: 'Anguilla' }, { value: 'AQ', text: 'Antarctica' }, { value: 'AG', text: 'Antigua and Barbuda' }, { value: 'AR', text: 'Argentina' }, { value: 'AM', text: 'Armenia' }, { value: 'AW', text: 'Aruba' }, { value: 'AU', text: 'Australia' }, { value: 'AT', text: 'Austria' }, { value: 'AZ', text: 'Azerbaijan' }, { value: 'BS', text: 'Bahamas' }, { value: 'BH', text: 'Bahrain' }, { value: 'BD', text: 'Bangladesh' }, { value: 'BB', text: 'Barbados' }, { value: 'BY', text: 'Belarus' }, { value: 'BE', text: 'Belgium' }, { value: 'BZ', text: 'Belize' }, { value: 'BJ', text: 'Benin' }, { value: 'BM', text: 'Bermuda' }, { value: 'BT', text: 'Bhutan' }, { value: 'BO', text: 'Bolivia' }, { value: 'BA', text: 'Bosnia and Herzegovina' }, { value: 'BW', text: 'Botswana' }, { value: 'BV', text: 'Bouvet Island' }, { value: 'BR', text: 'Brazil' }, { value: 'IO', text: 'British Indian Ocean Territory' }, { value: 'VG', text: 'British Virgin Islands' }, { value: 'BN', text: 'Brunei' }, { value: 'BG', text: 'Bulgaria' }, { value: 'BF', text: 'Burkina Faso' }, { value: 'BI', text: 'Burundi' }, { value: 'KH', text: 'Cambodia' }, { value: 'CM', text: 'Cameroon' }, { value: 'CA', text: 'Canada' }, { value: 'CV', text: 'Cape Verde' }, { value: 'KY', text: 'Cayman Islands' }, { value: 'CF', text: 'Central African Republic' }, { value: 'TD', text: 'Chad' }, { value: 'CL', text: 'Chile' }, { value: 'CN', text: 'China' }, { value: 'CX', text: 'Christmas Island' }, { value: 'CC', text: 'Cocos [Keeling] Islands' }, { value: 'CO', text: 'Colombia' }, { value: 'KM', text: 'Comoros' }, { value: 'CG', text: 'Congo - Brazzaville' }, { value: 'CD', text: 'Congo - Kinshasa' }, { value: 'CK', text: 'Cook Islands' }, { value: 'CR', text: 'Costa Rica' }, { value: 'HR', text: 'Croatia' }, { value: 'CU', text: 'Cuba' }, { value: 'CY', text: 'Cyprus' }, { value: 'CZ', text: 'Czech Republic' }, { value: 'CI', text: 'Côte d’Ivoire' }, { value: 'DK', text: 'Denmark' }, { value: 'DJ', text: 'Djibouti' }, { value: 'DM', text: 'Dominica' }, { value: 'DO', text: 'Dominican Republic' }, { value: 'EC', text: 'Ecuador' }, { value: 'EG', text: 'Egypt' }, { value: 'SV', text: 'El Salvador' }, { value: 'GQ', text: 'Equatorial Guinea' }, { value: 'ER', text: 'Eritrea' }, { value: 'EE', text: 'Estonia' }, { value: 'ET', text: 'Ethiopia' }, { value: 'FK', text: 'Falkland Islands' }, { value: 'FO', text: 'Faroe Islands' }, { value: 'FJ', text: 'Fiji' }, { value: 'FI', text: 'Finland' }, { value: 'FR', text: 'France' }, { value: 'GF', text: 'French Guiana' }, { value: 'PF', text: 'French Polynesia' }, { value: 'TF', text: 'French Southern Territories' }, { value: 'GA', text: 'Gabon' }, { value: 'GM', text: 'Gambia' }, { value: 'GE', text: 'Georgia' }, { value: 'DE', text: 'Germany' }, { value: 'GH', text: 'Ghana' }, { value: 'GI', text: 'Gibraltar' }, { value: 'GR', text: 'Greece' }, { value: 'GL', text: 'Greenland' }, { value: 'GD', text: 'Grenada' }, { value: 'GP', text: 'Guadeloupe' }, { value: 'GU', text: 'Guam' }, { value: 'GT', text: 'Guatemala' }, { value: 'GG', text: 'Guernsey' }, { value: 'GN', text: 'Guinea' }, { value: 'GW', text: 'Guinea-Bissau' }, { value: 'GY', text: 'Guyana' }, { value: 'HT', text: 'Haiti' }, { value: 'HM', text: 'Heard Island and McDonald Islands' }, { value: 'HN', text: 'Honduras' }, { value: 'HK', text: 'Hong Kong SAR China' }, { value: 'HU', text: 'Hungary' }, { value: 'IS', text: 'Iceland' }, { value: 'IN', text: 'India' }, { value: 'ID', text: 'Indonesia' }, { value: 'IR', text: 'Iran' }, { value: 'IQ', text: 'Iraq' }, { value: 'IE', text: 'Ireland' }, { value: 'IM', text: 'Isle of Man' }, { value: 'IL', text: 'Israel' }, { value: 'IT', text: 'Italy' }, { value: 'JM', text: 'Jamaica' }, { value: 'JP', text: 'Japan' }, { value: 'JE', text: 'Jersey' }, { value: 'JO', text: 'Jordan' }, { value: 'KZ', text: 'Kazakhstan' }, { value: 'KE', text: 'Kenya' }, { value: 'KI', text: 'Kiribati' }, { value: 'KW', text: 'Kuwait' }, { value: 'KG', text: 'Kyrgyzstan' }, { value: 'LA', text: 'Laos' }, { value: 'LV', text: 'Latvia' }, { value: 'LB', text: 'Lebanon' }, { value: 'LS', text: 'Lesotho' }, { value: 'LR', text: 'Liberia' }, { value: 'LY', text: 'Libya' }, { value: 'LI', text: 'Liechtenstein' }, { value: 'LT', text: 'Lithuania' }, { value: 'LU', text: 'Luxembourg' }, { value: 'MO', text: 'Macau SAR China' }, { value: 'MK', text: 'Macedonia' }, { value: 'MG', text: 'Madagascar' }, { value: 'MW', text: 'Malawi' }, { value: 'MY', text: 'Malaysia' }, { value: 'MV', text: 'Maldives' }, { value: 'ML', text: 'Mali' }, { value: 'MT', text: 'Malta' }, { value: 'MH', text: 'Marshall Islands' }, { value: 'MQ', text: 'Martinique' }, { value: 'MR', text: 'Mauritania' }, { value: 'MU', text: 'Mauritius' }, { value: 'YT', text: 'Mayotte' }, { value: 'MX', text: 'Mexico' }, { value: 'FM', text: 'Micronesia' }, { value: 'MD', text: 'Moldova' }, { value: 'MC', text: 'Monaco' }, { value: 'MN', text: 'Mongolia' }, { value: 'ME', text: 'Montenegro' }, { value: 'MS', text: 'Montserrat' }, { value: 'MA', text: 'Morocco' }, { value: 'MZ', text: 'Mozambique' }, { value: 'MM', text: 'Myanmar [Burma]' }, { value: 'NA', text: 'Namibia' }, { value: 'NR', text: 'Nauru' }, { value: 'NP', text: 'Nepal' }, { value: 'NL', text: 'Netherlands' }, { value: 'AN', text: 'Netherlands Antilles' }, { value: 'NC', text: 'New Caledonia' }, { value: 'NZ', text: 'New Zealand' }, { value: 'NI', text: 'Nicaragua' }, { value: 'NE', text: 'Niger' }, { value: 'NG', text: 'Nigeria' }, { value: 'NU', text: 'Niue' }, { value: 'NF', text: 'Norfolk Island' }, { value: 'KP', text: 'North Korea' }, { value: 'MP', text: 'Northern Mariana Islands' }, { value: 'NO', text: 'Norway' }, { value: 'OM', text: 'Oman' }, { value: 'PK', text: 'Pakistan' }, { value: 'PW', text: 'Palau' }, { value: 'PS', text: 'Palestinian Territories' }, { value: 'PA', text: 'Panama' }, { value: 'PG', text: 'Papua New Guinea' }, { value: 'PY', text: 'Paraguay' }, { value: 'PE', text: 'Peru' }, { value: 'PH', text: 'Philippines' }, { value: 'PN', text: 'Pitcairn Islands' }, { value: 'PL', text: 'Poland' }, { value: 'PT', text: 'Portugal' }, { value: 'PR', text: 'Puerto Rico' }, { value: 'QA', text: 'Qatar' }, { value: 'RO', text: 'Romania' }, { value: 'RU', text: 'Russia' }, { value: 'RW', text: 'Rwanda' }, { value: 'RE', text: 'Réunion' }, { value: 'BL', text: 'Saint Barthélemy' }, { value: 'SH', text: 'Saint Helena' }, { value: 'KN', text: 'Saint Kitts and Nevis' }, { value: 'LC', text: 'Saint Lucia' }, { value: 'MF', text: 'Saint Martin' }, { value: 'PM', text: 'Saint Pierre and Miquelon' }, { value: 'VC', text: 'Saint Vincent and the Grenadines' }, { value: 'WS', text: 'Samoa' }, { value: 'SM', text: 'San Marino' }, { value: 'SA', text: 'Saudi Arabia' }, { value: 'SN', text: 'Senegal' }, { value: 'RS', text: 'Serbia' }, { value: 'SC', text: 'Seychelles' }, { value: 'SL', text: 'Sierra Leone' }, { value: 'SG', text: 'Singapore' }, { value: 'SK', text: 'Slovakia' }, { value: 'SI', text: 'Slovenia' }, { value: 'SB', text: 'Solomon Islands' }, { value: 'SO', text: 'Somalia' }, { value: 'ZA', text: 'South Africa' }, { value: 'GS', text: 'South Georgia and the South Sandwich Islands' }, { value: 'KR', text: 'South Korea' }, { value: 'ES', text: 'Spain' }, { value: 'LK', text: 'Sri Lanka' }, { value: 'SD', text: 'Sudan' }, { value: 'SR', text: 'Suriname' }, { value: 'SJ', text: 'Svalbard and Jan Mayen' }, { value: 'SZ', text: 'Swaziland' }, { value: 'SE', text: 'Sweden' }, { value: 'CH', text: 'Switzerland' }, { value: 'SY', text: 'Syria' }, { value: 'ST', text: 'São Tomé and Príncipe' }, { value: 'TW', text: 'Taiwan' }, { value: 'TJ', text: 'Tajikistan' }, { value: 'TZ', text: 'Tanzania' }, { value: 'TH', text: 'Thailand' }, { value: 'TL', text: 'Timor-Leste' }, { value: 'TG', text: 'Togo' }, { value: 'TK', text: 'Tokelau' }, { value: 'TO', text: 'Tonga' }, { value: 'TT', text: 'Trinidad and Tobago' }, { value: 'TN', text: 'Tunisia' }, { value: 'TR', text: 'Turkey' }, { value: 'TM', text: 'Turkmenistan' }, { value: 'TC', text: 'Turks and Caicos Islands' }, { value: 'TV', text: 'Tuvalu' }, { value: 'UM', text: 'U.S. Minor Outlying Islands' }, { value: 'VI', text: 'U.S. Virgin Islands' }, { value: 'UG', text: 'Uganda' }, { value: 'UA', text: 'Ukraine' }, { value: 'AE', text: 'United Arab Emirates' }, { value: 'GB', text: 'United Kingdom' }, { value: 'US', text: 'United States' }, { value: 'UY', text: 'Uruguay' }, { value: 'UZ', text: 'Uzbekistan' }, { value: 'VU', text: 'Vanuatu' }, { value: 'VA', text: 'Vatican City' }, { value: 'VE', text: 'Venezuela' }, { value: 'VN', text: 'Vietnam' }, { value: 'WF', text: 'Wallis and Futuna' }, { value: 'EH', text: 'Western Sahara' }, { value: 'YE', text: 'Yemen' }, { value: 'ZM', text: 'Zambia' }, { value: 'ZW', text: 'Zimbabwe' }, { value: 'AX', text: 'Åland Islands' } ].filter((c) => { if (countries) { return countries.indexOf(c.value) !== -1; } else { return true; } }); const childrenWithProps = React.Children.map(children, (child) => React.cloneElement(child, { options, ...props }) ); return <div>{childrenWithProps}</div>; } CountryOptions.propTypes = { children: PropTypes.node.isRequired, countries: PropTypes.arrayOf(PropTypes.string) }; CountryOptions.defaultProps = { countries: [] }; export { CountryOptions }; ================================================ FILE: packages/evershop/src/components/common/locale/CurrencyOption.jsx ================================================ import PropTypes from 'prop-types'; import React from 'react'; function CurrencyOptions(props) { const { currencies, children } = props; const options = [ { value: 'AFN', text: 'Afghan Afghani' }, { value: 'ALL', text: 'Albanian Lek' }, { value: 'DZD', text: 'Algerian Dinar' }, { value: 'AOA', text: 'Angolan Kwanza' }, { value: 'ARS', text: 'Argentine Peso' }, { value: 'AMD', text: 'Armenian Dram' }, { value: 'AWG', text: 'Aruban Florin' }, { value: 'AUD', text: 'Australian Dollar' }, { value: 'AZN', text: 'Azerbaijani Manat' }, { value: 'AZM', text: 'Azerbaijani Manat (1993-2006)' }, { value: 'BSD', text: 'Bahamian Dollar' }, { value: 'BHD', text: 'Bahraini Dinar' }, { value: 'BDT', text: 'Bangladeshi Taka' }, { value: 'BBD', text: 'Barbadian Dollar' }, { value: 'BYR', text: 'Belarusian Ruble' }, { value: 'BZD', text: 'Belize Dollar' }, { value: 'BMD', text: 'Bermudan Dollar' }, { value: 'BTN', text: 'Bhutanese Ngultrum' }, { value: 'BOB', text: 'Bolivian Boliviano' }, { value: 'BAM', text: 'Bosnia-Herzegovina Convertible Mark' }, { value: 'BWP', text: 'Botswanan Pula' }, { value: 'BRL', text: 'Brazilian Real' }, { value: 'GBP', text: 'British Pound Sterling' }, { value: 'BND', text: 'Brunei Dollar' }, { value: 'BGN', text: 'Bulgarian Lev' }, { value: 'BUK', text: 'Burmese Kyat' }, { value: 'BIF', text: 'Burundian Franc' }, { value: 'XOF', text: 'CFA Franc BCEAO' }, { value: 'XPF', text: 'CFP Franc' }, { value: 'KHR', text: 'Cambodian Riel' }, { value: 'CAD', text: 'Canadian Dollar' }, { value: 'CVE', text: 'Cape Verdean Escudo' }, { value: 'KYD', text: 'Cayman Islands Dollar' }, { value: 'CLP', text: 'Chilean Peso' }, { value: 'CNY', text: 'Chinese Yuan Renminbi' }, { value: 'COP', text: 'Colombian Peso' }, { value: 'KMF', text: 'Comorian Franc' }, { value: 'CDF', text: 'Congolese Franc' }, { value: 'CRC', text: 'Costa Rican Colón' }, { value: 'HRK', text: 'Croatian Kuna' }, { value: 'CUP', text: 'Cuban Peso' }, { value: 'CZK', text: 'Czech Republic Koruna' }, { value: 'DKK', text: 'Danish Krone' }, { value: 'DJF', text: 'Djiboutian Franc' }, { value: 'DOP', text: 'Dominican Peso' }, { value: 'XCD', text: 'East Caribbean Dollar' }, { value: 'EGP', text: 'Egyptian Pound' }, { value: 'GQE', text: 'Equatorial Guinean Ekwele' }, { value: 'ERN', text: 'Eritrean Nakfa' }, { value: 'EEK', text: 'Estonian Kroon' }, { value: 'ETB', text: 'Ethiopian Birr' }, { value: 'EUR', text: 'Euro' }, { value: 'FKP', text: 'Falkland Islands Pound' }, { value: 'FJD', text: 'Fijian Dollar' }, { value: 'GMD', text: 'Gambian Dalasi' }, { value: 'GEK', text: 'Georgian Kupon Larit' }, { value: 'GEL', text: 'Georgian Lari' }, { value: 'GHS', text: 'Ghanaian Cedi' }, { value: 'GIP', text: 'Gibraltar Pound' }, { value: 'GTQ', text: 'Guatemalan Quetzal' }, { value: 'GNF', text: 'Guinean Franc' }, { value: 'GYD', text: 'Guyanaese Dollar' }, { value: 'HTG', text: 'Haitian Gourde' }, { value: 'HNL', text: 'Honduran Lempira' }, { value: 'HKD', text: 'Hong Kong Dollar' }, { value: 'HUF', text: 'Hungarian Forint' }, { value: 'ISK', text: 'Icelandic Króna' }, { value: 'INR', text: 'Indian Rupee' }, { value: 'IDR', text: 'Indonesian Rupiah' }, { value: 'IRR', text: 'Iranian Rial' }, { value: 'IQD', text: 'Iraqi Dinar' }, { value: 'ILS', text: 'Israeli New Sheqel' }, { value: 'JMD', text: 'Jamaican Dollar' }, { value: 'JPY', text: 'Japanese Yen' }, { value: 'JOD', text: 'Jordanian Dinar' }, { value: 'KZT', text: 'Kazakhstan Tenge' }, { value: 'KES', text: 'Kenyan Shilling' }, { value: 'KWD', text: 'Kuwaiti Dinar' }, { value: 'KGS', text: 'Kyrgystani Som' }, { value: 'LAK', text: 'Laotian Kip' }, { value: 'LVL', text: 'Latvian Lats' }, { value: 'LBP', text: 'Lebanese Pound' }, { value: 'LSL', text: 'Lesotho Loti' }, { value: 'LRD', text: 'Liberian Dollar' }, { value: 'LYD', text: 'Libyan Dinar' }, { value: 'LTL', text: 'Lithuanian Litas' }, { value: 'MOP', text: 'Macanese Pataca' }, { value: 'MKD', text: 'Macedonian Denar' }, { value: 'MGA', text: 'Malagasy Ariary' }, { value: 'MWK', text: 'Malawian Kwacha' }, { value: 'MYR', text: 'Malaysian Ringgit' }, { value: 'MVR', text: 'Maldivian Rufiyaa' }, { value: 'MRO', text: 'Mauritanian Ouguiya' }, { value: 'MUR', text: 'Mauritian Rupee' }, { value: 'MXN', text: 'Mexican Peso' }, { value: 'MDL', text: 'Moldovan Leu' }, { value: 'MNT', text: 'Mongolian Tugrik' }, { value: 'MAD', text: 'Moroccan Dirham' }, { value: 'MZN', text: 'Mozambican Metical' }, { value: 'MMK', text: 'Myanma Kyat' }, { value: 'NAD', text: 'Namibian Dollar' }, { value: 'NPR', text: 'Nepalese Rupee' }, { value: 'ANG', text: 'Netherlands Antillean Guilder' }, { value: 'TWD', text: 'New Taiwan Dollar' }, { value: 'NZD', text: 'New Zealand Dollar' }, { value: 'NIC', text: 'Nicaraguan Cordoba' }, { value: 'NGN', text: 'Nigerian Naira' }, { value: 'KPW', text: 'North Korean Won' }, { value: 'NOK', text: 'Norwegian Krone' }, { value: 'ROL', text: 'Old Romanian Leu' }, { value: 'TRL', text: 'Old Turkish Lira' }, { value: 'OMR', text: 'Omani Rial' }, { value: 'PKR', text: 'Pakistani Rupee' }, { value: 'PAB', text: 'Panamanian Balboa' }, { value: 'PGK', text: 'Papua New Guinean Kina' }, { value: 'PYG', text: 'Paraguayan Guarani' }, { value: 'PEN', text: 'Peruvian Nuevo Sol' }, { value: 'PHP', text: 'Philippine Peso' }, { value: 'PLN', text: 'Polish Zloty' }, { value: 'QAR', text: 'Qatari Rial' }, { value: 'RHD', text: 'Rhodesian Dollar' }, { value: 'RON', text: 'Romanian Leu' }, { value: 'RUB', text: 'Russian Ruble' }, { value: 'RWF', text: 'Rwandan Franc' }, { value: 'SHP', text: 'Saint Helena Pound' }, { value: 'SVC', text: 'Salvadoran Colón' }, { value: 'WST', text: 'Samoan Tala' }, { value: 'SAR', text: 'Saudi Riyal' }, { value: 'RSD', text: 'Serbian Dinar' }, { value: 'SCR', text: 'Seychellois Rupee' }, { value: 'SLL', text: 'Sierra Leonean Leone' }, { value: 'SGD', text: 'Singapore Dollar' }, { value: 'SKK', text: 'Slovak Koruna' }, { value: 'SBD', text: 'Solomon Islands Dollar' }, { value: 'SOS', text: 'Somali Shilling' }, { value: 'ZAR', text: 'South African Rand' }, { value: 'KRW', text: 'South Korean Won' }, { value: 'LKR', text: 'Sri Lanka Rupee' }, { value: 'SDG', text: 'Sudanese Pound' }, { value: 'SRD', text: 'Surinamese Dollar' }, { value: 'SZL', text: 'Swazi Lilangeni' }, { value: 'SEK', text: 'Swedish Krona' }, { value: 'CHF', text: 'Swiss Franc' }, { value: 'SYP', text: 'Syrian Pound' }, { value: 'STD', text: 'São Tomé and Príncipe Dobra' }, { value: 'TJS', text: 'Tajikistani Somoni' }, { value: 'TZS', text: 'Tanzanian Shilling' }, { value: 'THB', text: 'Thai Baht' }, { value: 'TOP', text: 'Tongan Paʻanga' }, { value: 'TTD', text: 'Trinidad and Tobago Dollar' }, { value: 'TND', text: 'Tunisian Dinar' }, { value: 'TRY', text: 'Turkish Lira' }, { value: 'TMM', text: 'Turkmenistani Manat' }, { value: 'USD', text: 'US Dollar' }, { value: 'UGX', text: 'Ugandan Shilling' }, { value: 'UAH', text: 'Ukrainian Hryvnia' }, { value: 'AED', text: 'United Arab Emirates Dirham' }, { value: 'UYU', text: 'Uruguayan Peso' }, { value: 'UZS', text: 'Uzbekistan Som' }, { value: 'VUV', text: 'Vanuatu Vatu' }, { value: 'VEB', text: 'Venezuelan Bolívar' }, { value: 'VEF', text: 'Venezuelan Bolívar Fuerte' }, { value: 'VND', text: 'Vietnamese Dong' }, { value: 'CHE', text: 'WIR Euro' }, { value: 'CHW', text: 'WIR Franc' }, { value: 'YER', text: 'Yemeni Rial' }, { value: 'ZMK', text: 'Zambian Kwacha' }, { value: 'ZWD', text: 'Zimbabwean Dollar' } ].filter((c) => { if (currencies) return currencies.indexOf(c.value) !== -1; else return true; }); const childrenWithProps = React.Children.map(children, (child) => React.cloneElement(child, { options, ...props }) ); return <div>{childrenWithProps}</div>; } CurrencyOptions.propTypes = { children: PropTypes.node.isRequired, currencies: PropTypes.arrayOf(PropTypes.string) }; CurrencyOptions.defaultProps = { currencies: [] }; export { CurrencyOptions }; ================================================ FILE: packages/evershop/src/components/common/locale/LanguageOption.jsx ================================================ import PropTypes from 'prop-types'; import React from 'react'; function LanguageOptions(props) { const { languages, children } = props; const options = [ { value: 'aa', text: 'Afar' }, { value: 'ab', text: 'Abkhazian' }, { value: 'ae', text: 'Avestan' }, { value: 'af', text: 'Afrikaans' }, { value: 'ak', text: 'Akan' }, { value: 'am', text: 'Amharic' }, { value: 'an', text: 'Aragonese' }, { value: 'ar', text: 'Arabic' }, { value: 'as', text: 'Assamese' }, { value: 'av', text: 'Avaric' }, { value: 'ay', text: 'Aymara' }, { value: 'az', text: 'Azerbaijani' }, { value: 'ba', text: 'Bashkir' }, { value: 'be', text: 'Belarusian' }, { value: 'bg', text: 'Bulgarian' }, { value: 'bh', text: 'Bihari languages' }, { value: 'bi', text: 'Bislama' }, { value: 'bm', text: 'Bambara' }, { value: 'bn', text: 'Bengali' }, { value: 'bo', text: 'Tibetan' }, { value: 'br', text: 'Breton' }, { value: 'bs', text: 'Bosnian' }, { value: 'ca', text: 'Catalan; Valencian' }, { value: 'ce', text: 'Chechen' }, { value: 'ch', text: 'Chamorro' }, { value: 'co', text: 'Corsican' }, { value: 'cr', text: 'Cree' }, { value: 'cs', text: 'Czech' }, { value: 'cu', text: 'Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic' }, { value: 'cv', text: 'Chuvash' }, { value: 'cy', text: 'Welsh' }, { value: 'da', text: 'Danish' }, { value: 'de', text: 'German' }, { value: 'dv', text: 'Divehi; Dhivehi; Maldivian' }, { value: 'dz', text: 'Dzongkha' }, { value: 'ee', text: 'Ewe' }, { value: 'el', text: 'Greek, Modern (1453-)' }, { value: 'en', text: 'English' }, { value: 'eo', text: 'Esperanto' }, { value: 'es', text: 'Spanish; Castilian' }, { value: 'et', text: 'Estonian' }, { value: 'eu', text: 'Basque' }, { value: 'fa', text: 'Persian' }, { value: 'ff', text: 'Fulah' }, { value: 'fi', text: 'Finnish' }, { value: 'fj', text: 'Fijian' }, { value: 'fo', text: 'Faroese' }, { value: 'fr', text: 'French' }, { value: 'fy', text: 'Western Frisian' }, { value: 'ga', text: 'Irish' }, { value: 'gd', text: 'Gaelic; Scottish Gaelic' }, { value: 'gl', text: 'Galician' }, { value: 'gn', text: 'Guarani' }, { value: 'gu', text: 'Gujarati' }, { value: 'gv', text: 'Manx' }, { value: 'ha', text: 'Hausa' }, { value: 'he', text: 'Hebrew' }, { value: 'hi', text: 'Hindi' }, { value: 'ho', text: 'Hiri Motu' }, { value: 'hr', text: 'Croatian' }, { value: 'ht', text: 'Haitian; Haitian Creole' }, { value: 'hu', text: 'Hungarian' }, { value: 'hy', text: 'Armenian' }, { value: 'hz', text: 'Herero' }, { value: 'ia', text: 'Interlingua (International Auxiliary Language Association)' }, { value: 'id', text: 'Indonesian' }, { value: 'ie', text: 'Interlingue; Occidental' }, { value: 'ig', text: 'Igbo' }, { value: 'ii', text: 'Sichuan Yi; Nuosu' }, { value: 'ik', text: 'Inupiaq' }, { value: 'io', text: 'Ido' }, { value: 'is', text: 'Icelandic' }, { value: 'it', text: 'Italian' }, { value: 'iu', text: 'Inuktitut' }, { value: 'ja', text: 'Japanese' }, { value: 'jv', text: 'Javanese' }, { value: 'ka', text: 'Georgian' }, { value: 'kg', text: 'Kongo' }, { value: 'ki', text: 'Kikuyu; Gikuyu' }, { value: 'kj', text: 'Kuanyama; Kwanyama' }, { value: 'kk', text: 'Kazakh' }, { value: 'kl', text: 'Kalaallisut; Greenlandic' }, { value: 'km', text: 'Central Khmer' }, { value: 'kn', text: 'Kannada' }, { value: 'ko', text: 'Korean' }, { value: 'kr', text: 'Kanuri' }, { value: 'ks', text: 'Kashmiri' }, { value: 'ku', text: 'Kurdish' }, { value: 'kv', text: 'Komi' }, { value: 'kw', text: 'Cornish' }, { value: 'ky', text: 'Kirghiz; Kyrgyz' }, { value: 'la', text: 'Latin' }, { value: 'lb', text: 'Luxembourgish; Letzeburgesch' }, { value: 'lg', text: 'Ganda' }, { value: 'li', text: 'Limburgan; Limburger; Limburgish' }, { value: 'ln', text: 'Lingala' }, { value: 'lo', text: 'Lao' }, { value: 'lt', text: 'Lithuanian' }, { value: 'lu', text: 'Luba-Katanga' }, { value: 'lv', text: 'Latvian' }, { value: 'mg', text: 'Malagasy' }, { value: 'mh', text: 'Marshallese' }, { value: 'mi', text: 'Maori' }, { value: 'mk', text: 'Macedonian' }, { value: 'ml', text: 'Malayalam' }, { value: 'mn', text: 'Mongolian' }, { value: 'mr', text: 'Marathi' }, { value: 'ms', text: 'Malay' }, { value: 'mt', text: 'Maltese' }, { value: 'my', text: 'Burmese' }, { value: 'na', text: 'Nauru' }, { value: 'nb', text: 'Bokmål, Norwegian; Norwegian Bokmål' }, { value: 'nd', text: 'Ndebele, North; North Ndebele' }, { value: 'ne', text: 'Nepali' }, { value: 'ng', text: 'Ndonga' }, { value: 'nl', text: 'Dutch; Flemish' }, { value: 'nn', text: 'Norwegian Nynorsk; Nynorsk, Norwegian' }, { value: 'no', text: 'Norwegian' }, { value: 'nr', text: 'Ndebele, South; South Ndebele' }, { value: 'nv', text: 'Navajo; Navaho' }, { value: 'ny', text: 'Chichewa; Chewa; Nyanja' }, { value: 'oc', text: 'Occitan (post 1500)' }, { value: 'oj', text: 'Ojibwa' }, { value: 'om', text: 'Oromo' }, { value: 'or', text: 'Oriya' }, { value: 'os', text: 'Ossetian; Ossetic' }, { value: 'pa', text: 'Panjabi; Punjabi' }, { value: 'pi', text: 'Pali' }, { value: 'pl', text: 'Polish' }, { value: 'ps', text: 'Pushto; Pashto' }, { value: 'pt', text: 'Portuguese' }, { value: 'qu', text: 'Quechua' }, { value: 'rm', text: 'Romansh' }, { value: 'rn', text: 'Rundi' }, { value: 'ro', text: 'Romanian; Moldavian; Moldovan' }, { value: 'ru', text: 'Russian' }, { value: 'rw', text: 'Kinyarwanda' }, { value: 'sa', text: 'Sanskrit' }, { value: 'sc', text: 'Sardinian' }, { value: 'sd', text: 'Sindhi' }, { value: 'se', text: 'Northern Sami' }, { value: 'sg', text: 'Sango' }, { value: 'si', text: 'Sinhala; Sinhalese' }, { value: 'sk', text: 'Slovak' }, { value: 'sl', text: 'Slovenian' }, { value: 'sm', text: 'Samoan' }, { value: 'sn', text: 'Shona' }, { value: 'so', text: 'Somali' }, { value: 'sq', text: 'Albanian' }, { value: 'sr', text: 'Serbian' }, { value: 'ss', text: 'Swati' }, { value: 'st', text: 'Sotho, Southern' }, { value: 'su', text: 'Sundanese' }, { value: 'sv', text: 'Swedish' }, { value: 'sw', text: 'Swahili' }, { value: 'ta', text: 'Tamil' }, { value: 'te', text: 'Telugu' }, { value: 'tg', text: 'Tajik' }, { value: 'th', text: 'Thai' }, { value: 'ti', text: 'Tigrinya' }, { value: 'tk', text: 'Turkmen' }, { value: 'tl', text: 'Tagalog' }, { value: 'tn', text: 'Tswana' }, { value: 'to', text: 'Tonga (Tonga Islands)' }, { value: 'tr', text: 'Turkish' }, { value: 'ts', text: 'Tsonga' }, { value: 'tt', text: 'Tatar' }, { value: 'tw', text: 'Twi' }, { value: 'ty', text: 'Tahitian' }, { value: 'ug', text: 'Uighur; Uyghur' }, { value: 'uk', text: 'Ukrainian' }, { value: 'ur', text: 'Urdu' }, { value: 'uz', text: 'Uzbek' }, { value: 've', text: 'Venda' }, { value: 'vi', text: 'Vietnamese' }, { value: 'vo', text: 'Volapük' }, { value: 'wa', text: 'Walloon' }, { value: 'wo', text: 'Wolof' }, { value: 'xh', text: 'Xhosa' }, { value: 'yi', text: 'Yiddish' }, { value: 'yo', text: 'Yoruba' }, { value: 'za', text: 'Zhuang; Chuang' }, { value: 'zh', text: 'Chinese' }, { value: 'zu', text: 'Zulu' } ].filter((l) => { if (languages) return languages.indexOf(l.value) !== -1; else return true; }); const childrenWithProps = React.Children.map(children, (child) => React.cloneElement(child, { options, ...props }) ); return <div>{childrenWithProps}</div>; } LanguageOptions.propTypes = { children: PropTypes.node.isRequired, languages: PropTypes.arrayOf(PropTypes.string) }; LanguageOptions.defaultProps = { languages: [] }; export { LanguageOptions }; ================================================ FILE: packages/evershop/src/components/common/locale/ProvinceOption.jsx ================================================ import PropTypes from 'prop-types'; import React from 'react'; function ProvinceOptions(props) { const { country, children } = props; const options = [ { value: 'AD-07', country_code: 'AD', text: 'Andorra la Vella' }, { value: 'AD-02', country_code: 'AD', text: 'Canillo' }, { value: 'AD-03', country_code: 'AD', text: 'Encamp' }, { value: 'AD-08', country_code: 'AD', text: 'Escaldes-Engordany' }, { value: 'AD-04', country_code: 'AD', text: 'La Massana' }, { value: 'AD-05', country_code: 'AD', text: 'Ordino' }, { value: 'AD-06', country_code: 'AD', text: 'Sant Julia de Loria' }, { value: 'AE-AJ', country_code: 'AE', text: "'Ajman" }, { value: 'AE-AZ', country_code: 'AE', text: 'Abu Zaby' }, { value: 'AE-FU', country_code: 'AE', text: 'Al Fujayrah' }, { value: 'AE-SH', country_code: 'AE', text: 'Ash Shariqah' }, { value: 'AE-DU', country_code: 'AE', text: 'Dubayy' }, { value: 'AE-RK', country_code: 'AE', text: "Ra's al Khaymah" }, { value: 'AE-UQ', country_code: 'AE', text: 'Umm al Qaywayn' }, { value: 'AF-BDS', country_code: 'AF', text: 'Badakhshan' }, { value: 'AF-BDG', country_code: 'AF', text: 'Badghis' }, { value: 'AF-BGL', country_code: 'AF', text: 'Baghlan' }, { value: 'AF-BAL', country_code: 'AF', text: 'Balkh' }, { value: 'AF-BAM', country_code: 'AF', text: 'Bamyan' }, { value: 'AF-DAY', country_code: 'AF', text: 'Daykundi' }, { value: 'AF-FRA', country_code: 'AF', text: 'Farah' }, { value: 'AF-FYB', country_code: 'AF', text: 'Faryab' }, { value: 'AF-GHA', country_code: 'AF', text: 'Ghazni' }, { value: 'AF-GHO', country_code: 'AF', text: 'Ghor' }, { value: 'AF-HEL', country_code: 'AF', text: 'Helmand' }, { value: 'AF-HER', country_code: 'AF', text: 'Herat' }, { value: 'AF-JOW', country_code: 'AF', text: 'Jowzjan' }, { value: 'AF-KAB', country_code: 'AF', text: 'Kabul' }, { value: 'AF-KAN', country_code: 'AF', text: 'Kandahar' }, { value: 'AF-KAP', country_code: 'AF', text: 'Kapisa' }, { value: 'AF-KHO', country_code: 'AF', text: 'Khost' }, { value: 'AF-KNR', country_code: 'AF', text: 'Kunar' }, { value: 'AF-KDZ', country_code: 'AF', text: 'Kunduz' }, { value: 'AF-LAG', country_code: 'AF', text: 'Laghman' }, { value: 'AF-LOG', country_code: 'AF', text: 'Logar' }, { value: 'AF-NAN', country_code: 'AF', text: 'Nangarhar' }, { value: 'AF-NIM', country_code: 'AF', text: 'Nimroz' }, { value: 'AF-NUR', country_code: 'AF', text: 'Nuristan' }, { value: 'AF-PKA', country_code: 'AF', text: 'Paktika' }, { value: 'AF-PIA', country_code: 'AF', text: 'Paktiya' }, { value: 'AF-PAN', country_code: 'AF', text: 'Panjshayr' }, { value: 'AF-PAR', country_code: 'AF', text: 'Parwan' }, { value: 'AF-SAM', country_code: 'AF', text: 'Samangan' }, { value: 'AF-SAR', country_code: 'AF', text: 'Sar-e Pul' }, { value: 'AF-TAK', country_code: 'AF', text: 'Takhar' }, { value: 'AF-URU', country_code: 'AF', text: 'Uruzgan' }, { value: 'AF-WAR', country_code: 'AF', text: 'Wardak' }, { value: 'AF-ZAB', country_code: 'AF', text: 'Zabul' }, { value: 'AG-04', country_code: 'AG', text: 'Saint John' }, { value: 'AG-05', country_code: 'AG', text: 'Saint Mary' }, { value: 'AG-06', country_code: 'AG', text: 'Saint Paul' }, { value: 'AL-01', country_code: 'AL', text: 'Berat' }, { value: 'AL-09', country_code: 'AL', text: 'Diber' }, { value: 'AL-02', country_code: 'AL', text: 'Durres' }, { value: 'AL-03', country_code: 'AL', text: 'Elbasan' }, { value: 'AL-04', country_code: 'AL', text: 'Fier' }, { value: 'AL-05', country_code: 'AL', text: 'Gjirokaster' }, { value: 'AL-06', country_code: 'AL', text: 'Korce' }, { value: 'AL-07', country_code: 'AL', text: 'Kukes' }, { value: 'AL-08', country_code: 'AL', text: 'Lezhe' }, { value: 'AL-10', country_code: 'AL', text: 'Shkoder' }, { value: 'AL-11', country_code: 'AL', text: 'Tirane' }, { value: 'AL-12', country_code: 'AL', text: 'Vlore' }, { value: 'AM-AG', country_code: 'AM', text: 'Aragacotn' }, { value: 'AM-AR', country_code: 'AM', text: 'Ararat' }, { value: 'AM-AV', country_code: 'AM', text: 'Armavir' }, { value: 'AM-ER', country_code: 'AM', text: 'Erevan' }, { value: 'AM-GR', country_code: 'AM', text: "Gegark'unik'" }, { value: 'AM-KT', country_code: 'AM', text: "Kotayk'" }, { value: 'AM-LO', country_code: 'AM', text: 'Lori' }, { value: 'AM-SH', country_code: 'AM', text: 'Sirak' }, { value: 'AM-SU', country_code: 'AM', text: "Syunik'" }, { value: 'AM-TV', country_code: 'AM', text: 'Tavus' }, { value: 'AM-VD', country_code: 'AM', text: 'Vayoc Jor' }, { value: 'AO-BGO', country_code: 'AO', text: 'Bengo' }, { value: 'AO-BGU', country_code: 'AO', text: 'Benguela' }, { value: 'AO-BIE', country_code: 'AO', text: 'Bie' }, { value: 'AO-CAB', country_code: 'AO', text: 'Cabinda' }, { value: 'AO-CNN', country_code: 'AO', text: 'Cunene' }, { value: 'AO-HUA', country_code: 'AO', text: 'Huambo' }, { value: 'AO-HUI', country_code: 'AO', text: 'Huila' }, { value: 'AO-CCU', country_code: 'AO', text: 'Kuando Kubango' }, { value: 'AO-CNO', country_code: 'AO', text: 'Kwanza Norte' }, { value: 'AO-CUS', country_code: 'AO', text: 'Kwanza Sul' }, { value: 'AO-LUA', country_code: 'AO', text: 'Luanda' }, { value: 'AO-LNO', country_code: 'AO', text: 'Lunda Norte' }, { value: 'AO-LSU', country_code: 'AO', text: 'Lunda Sul' }, { value: 'AO-MAL', country_code: 'AO', text: 'Malange' }, { value: 'AO-MOX', country_code: 'AO', text: 'Moxico' }, { value: 'AO-NAM', country_code: 'AO', text: 'Namibe' }, { value: 'AO-UIG', country_code: 'AO', text: 'Uige' }, { value: 'AO-ZAI', country_code: 'AO', text: 'Zaire' }, { value: 'AR-B', country_code: 'AR', text: 'Buenos Aires' }, { value: 'AR-K', country_code: 'AR', text: 'Catamarca' }, { value: 'AR-H', country_code: 'AR', text: 'Chaco' }, { value: 'AR-U', country_code: 'AR', text: 'Chubut' }, { value: 'AR-C', country_code: 'AR', text: 'Ciudad Autonoma de Buenos Aires' }, { value: 'AR-X', country_code: 'AR', text: 'Cordoba' }, { value: 'AR-W', country_code: 'AR', text: 'Corrientes' }, { value: 'AR-E', country_code: 'AR', text: 'Entre Rios' }, { value: 'AR-P', country_code: 'AR', text: 'Formosa' }, { value: 'AR-Y', country_code: 'AR', text: 'Jujuy' }, { value: 'AR-L', country_code: 'AR', text: 'La Pampa' }, { value: 'AR-F', country_code: 'AR', text: 'La Rioja' }, { value: 'AR-M', country_code: 'AR', text: 'Mendoza' }, { value: 'AR-N', country_code: 'AR', text: 'Misiones' }, { value: 'AR-Q', country_code: 'AR', text: 'Neuquen' }, { value: 'AR-R', country_code: 'AR', text: 'Rio Negro' }, { value: 'AR-A', country_code: 'AR', text: 'Salta' }, { value: 'AR-J', country_code: 'AR', text: 'San Juan' }, { value: 'AR-D', country_code: 'AR', text: 'San Luis' }, { value: 'AR-Z', country_code: 'AR', text: 'Santa Cruz' }, { value: 'AR-S', country_code: 'AR', text: 'Santa Fe' }, { value: 'AR-G', country_code: 'AR', text: 'Santiago del Estero' }, { value: 'AR-V', country_code: 'AR', text: 'Tierra del Fuego' }, { value: 'AR-T', country_code: 'AR', text: 'Tucuman' }, { value: 'AT-1', country_code: 'AT', text: 'Burgenland' }, { value: 'AT-2', country_code: 'AT', text: 'Karnten' }, { value: 'AT-3', country_code: 'AT', text: 'Niederosterreich' }, { value: 'AT-4', country_code: 'AT', text: 'Oberosterreich' }, { value: 'AT-5', country_code: 'AT', text: 'Salzburg' }, { value: 'AT-6', country_code: 'AT', text: 'Steiermark' }, { value: 'AT-7', country_code: 'AT', text: 'Tirol' }, { value: 'AT-8', country_code: 'AT', text: 'Vorarlberg' }, { value: 'AT-9', country_code: 'AT', text: 'Wien' }, { value: 'AU-ACT', country_code: 'AU', text: 'Australian Capital Territory' }, { value: 'AU-NSW', country_code: 'AU', text: 'New South Wales' }, { value: 'AU-NT', country_code: 'AU', text: 'Northern Territory' }, { value: 'AU-QLD', country_code: 'AU', text: 'Queensland' }, { value: 'AU-SA', country_code: 'AU', text: 'South Australia' }, { value: 'AU-TAS', country_code: 'AU', text: 'Tasmania' }, { value: 'AU-VIC', country_code: 'AU', text: 'Victoria' }, { value: 'AU-WA', country_code: 'AU', text: 'Western Australia' }, { value: 'AZ-ABS', country_code: 'AZ', text: 'Abseron' }, { value: 'AZ-AGC', country_code: 'AZ', text: 'Agcabadi' }, { value: 'AZ-AGM', country_code: 'AZ', text: 'Agdam' }, { value: 'AZ-AGS', country_code: 'AZ', text: 'Agdas' }, { value: 'AZ-AGA', country_code: 'AZ', text: 'Agstafa' }, { value: 'AZ-AGU', country_code: 'AZ', text: 'Agsu' }, { value: 'AZ-AST', country_code: 'AZ', text: 'Astara' }, { value: 'AZ-BA', country_code: 'AZ', text: 'Baki' }, { value: 'AZ-BAL', country_code: 'AZ', text: 'Balakan' }, { value: 'AZ-BAR', country_code: 'AZ', text: 'Barda' }, { value: 'AZ-BEY', country_code: 'AZ', text: 'Beylaqan' }, { value: 'AZ-BIL', country_code: 'AZ', text: 'Bilasuvar' }, { value: 'AZ-CAB', country_code: 'AZ', text: 'Cabrayil' }, { value: 'AZ-CAL', country_code: 'AZ', text: 'Calilabad' }, { value: 'AZ-DAS', country_code: 'AZ', text: 'Daskasan' }, { value: 'AZ-FUZ', country_code: 'AZ', text: 'Fuzuli' }, { value: 'AZ-GAD', country_code: 'AZ', text: 'Gadabay' }, { value: 'AZ-GA', country_code: 'AZ', text: 'Ganca' }, { value: 'AZ-GOR', country_code: 'AZ', text: 'Goranboy' }, { value: 'AZ-GOY', country_code: 'AZ', text: 'Goycay' }, { value: 'AZ-GYG', country_code: 'AZ', text: 'Goygol' }, { value: 'AZ-HAC', country_code: 'AZ', text: 'Haciqabul' }, { value: 'AZ-IMI', country_code: 'AZ', text: 'Imisli' }, { value: 'AZ-ISM', country_code: 'AZ', text: 'Ismayilli' }, { value: 'AZ-KAL', country_code: 'AZ', text: 'Kalbacar' }, { value: 'AZ-LAC', country_code: 'AZ', text: 'Lacin' }, { value: 'AZ-LA', country_code: 'AZ', text: 'Lankaran' }, { value: 'AZ-LER', country_code: 'AZ', text: 'Lerik' }, { value: 'AZ-MAS', country_code: 'AZ', text: 'Masalli' }, { value: 'AZ-MI', country_code: 'AZ', text: 'Mingacevir' }, { value: 'AZ-NA', country_code: 'AZ', text: 'Naftalan' }, { value: 'AZ-NX', country_code: 'AZ', text: 'Naxcivan' }, { value: 'AZ-NEF', country_code: 'AZ', text: 'Neftcala' }, { value: 'AZ-OGU', country_code: 'AZ', text: 'Oguz' }, { value: 'AZ-QAB', country_code: 'AZ', text: 'Qabala' }, { value: 'AZ-QAX', country_code: 'AZ', text: 'Qax' }, { value: 'AZ-QAZ', country_code: 'AZ', text: 'Qazax' }, { value: 'AZ-QOB', country_code: 'AZ', text: 'Qobustan' }, { value: 'AZ-QBA', country_code: 'AZ', text: 'Quba' }, { value: 'AZ-QBI', country_code: 'AZ', text: 'Qubadli' }, { value: 'AZ-QUS', country_code: 'AZ', text: 'Qusar' }, { value: 'AZ-SAT', country_code: 'AZ', text: 'Saatli' }, { value: 'AZ-SAB', country_code: 'AZ', text: 'Sabirabad' }, { value: 'AZ-SA', country_code: 'AZ', text: 'Saki' }, { value: 'AZ-SAL', country_code: 'AZ', text: 'Salyan' }, { value: 'AZ-SMI', country_code: 'AZ', text: 'Samaxi' }, { value: 'AZ-SKR', country_code: 'AZ', text: 'Samkir' }, { value: 'AZ-SMX', country_code: 'AZ', text: 'Samux' }, { value: 'AZ-SR', country_code: 'AZ', text: 'Sirvan' }, { value: 'AZ-SM', country_code: 'AZ', text: 'Sumqayit' }, { value: 'AZ-SUS', country_code: 'AZ', text: 'Susa' }, { value: 'AZ-TAR', country_code: 'AZ', text: 'Tartar' }, { value: 'AZ-TOV', country_code: 'AZ', text: 'Tovuz' }, { value: 'AZ-UCA', country_code: 'AZ', text: 'Ucar' }, { value: 'AZ-XAC', country_code: 'AZ', text: 'Xacmaz' }, { value: 'AZ-XA', country_code: 'AZ', text: 'Xankandi' }, { value: 'AZ-XIZ', country_code: 'AZ', text: 'Xizi' }, { value: 'AZ-XCI', country_code: 'AZ', text: 'Xocali' }, { value: 'AZ-XVD', country_code: 'AZ', text: 'Xocavand' }, { value: 'AZ-YAR', country_code: 'AZ', text: 'Yardimli' }, { value: 'AZ-YE', country_code: 'AZ', text: 'Yevlax' }, { value: 'AZ-ZAN', country_code: 'AZ', text: 'Zangilan' }, { value: 'AZ-ZAQ', country_code: 'AZ', text: 'Zaqatala' }, { value: 'AZ-ZAR', country_code: 'AZ', text: 'Zardab' }, { value: 'BA-BIH', country_code: 'BA', text: 'Federacija Bosne i Hercegovine' }, { value: 'BA-SRP', country_code: 'BA', text: 'Republika Srpska' }, { value: 'BB-01', country_code: 'BB', text: 'Christ Church' }, { value: 'BB-04', country_code: 'BB', text: 'Saint James' }, { value: 'BB-06', country_code: 'BB', text: 'Saint Joseph' }, { value: 'BB-08', country_code: 'BB', text: 'Saint Michael' }, { value: 'BB-09', country_code: 'BB', text: 'Saint Peter' }, { value: 'BD-06', country_code: 'BD', text: 'Barisal' }, { value: 'BD-10', country_code: 'BD', text: 'Chittagong' }, { value: 'BD-13', country_code: 'BD', text: 'Dhaka' }, { value: 'BD-27', country_code: 'BD', text: 'Khulna' }, { value: 'BD-54', country_code: 'BD', text: 'Rajshahi' }, { value: 'BD-55', country_code: 'BD', text: 'Rangpur' }, { value: 'BD-60', country_code: 'BD', text: 'Sylhet' }, { value: 'BE-VAN', country_code: 'BE', text: 'Antwerpen' }, { value: 'BE-WBR', country_code: 'BE', text: 'Brabant wallon' }, { value: 'BE-BRU', country_code: 'BE', text: 'Brussels Hoofdstedelijk Gewest' }, { value: 'BE-WHT', country_code: 'BE', text: 'Hainaut' }, { value: 'BE-WLG', country_code: 'BE', text: 'Liege' }, { value: 'BE-VLI', country_code: 'BE', text: 'Limburg' }, { value: 'BE-WLX', country_code: 'BE', text: 'Luxembourg' }, { value: 'BE-WNA', country_code: 'BE', text: 'Namur' }, { value: 'BE-VOV', country_code: 'BE', text: 'Oost-Vlaanderen' }, { value: 'BE-VBR', country_code: 'BE', text: 'Vlaams-Brabant' }, { value: 'BE-VWV', country_code: 'BE', text: 'West-Vlaanderen' }, { value: 'BF-BAL', country_code: 'BF', text: 'Bale' }, { value: 'BF-BAM', country_code: 'BF', text: 'Bam' }, { value: 'BF-BAN', country_code: 'BF', text: 'Banwa' }, { value: 'BF-BAZ', country_code: 'BF', text: 'Bazega' }, { value: 'BF-BGR', country_code: 'BF', text: 'Bougouriba' }, { value: 'BF-BLG', country_code: 'BF', text: 'Boulgou' }, { value: 'BF-BLK', country_code: 'BF', text: 'Boulkiemde' }, { value: 'BF-COM', country_code: 'BF', text: 'Comoe' }, { value: 'BF-GAN', country_code: 'BF', text: 'Ganzourgou' }, { value: 'BF-GNA', country_code: 'BF', text: 'Gnagna' }, { value: 'BF-GOU', country_code: 'BF', text: 'Gourma' }, { value: 'BF-HOU', country_code: 'BF', text: 'Houet' }, { value: 'BF-IOB', country_code: 'BF', text: 'Ioba' }, { value: 'BF-KAD', country_code: 'BF', text: 'Kadiogo' }, { value: 'BF-KEN', country_code: 'BF', text: 'Kenedougou' }, { value: 'BF-KMD', country_code: 'BF', text: 'Komondjari' }, { value: 'BF-KMP', country_code: 'BF', text: 'Kompienga' }, { value: 'BF-KOS', country_code: 'BF', text: 'Kossi' }, { value: 'BF-KOP', country_code: 'BF', text: 'Koulpelogo' }, { value: 'BF-KOT', country_code: 'BF', text: 'Kouritenga' }, { value: 'BF-KOW', country_code: 'BF', text: 'Kourweogo' }, { value: 'BF-LER', country_code: 'BF', text: 'Leraba' }, { value: 'BF-LOR', country_code: 'BF', text: 'Loroum' }, { value: 'BF-MOU', country_code: 'BF', text: 'Mouhoun' }, { value: 'BF-NAO', country_code: 'BF', text: 'Nahouri' }, { value: 'BF-NAM', country_code: 'BF', text: 'Namentenga' }, { value: 'BF-NAY', country_code: 'BF', text: 'Nayala' }, { value: 'BF-NOU', country_code: 'BF', text: 'Noumbiel' }, { value: 'BF-OUB', country_code: 'BF', text: 'Oubritenga' }, { value: 'BF-OUD', country_code: 'BF', text: 'Oudalan' }, { value: 'BF-PAS', country_code: 'BF', text: 'Passore' }, { value: 'BF-PON', country_code: 'BF', text: 'Poni' }, { value: 'BF-SNG', country_code: 'BF', text: 'Sanguie' }, { value: 'BF-SMT', country_code: 'BF', text: 'Sanmatenga' }, { value: 'BF-SEN', country_code: 'BF', text: 'Seno' }, { value: 'BF-SIS', country_code: 'BF', text: 'Sissili' }, { value: 'BF-SOM', country_code: 'BF', text: 'Soum' }, { value: 'BF-SOR', country_code: 'BF', text: 'Sourou' }, { value: 'BF-TAP', country_code: 'BF', text: 'Tapoa' }, { value: 'BF-TUI', country_code: 'BF', text: 'Tuy' }, { value: 'BF-YAG', country_code: 'BF', text: 'Yagha' }, { value: 'BF-YAT', country_code: 'BF', text: 'Yatenga' }, { value: 'BF-ZIR', country_code: 'BF', text: 'Ziro' }, { value: 'BF-ZON', country_code: 'BF', text: 'Zondoma' }, { value: 'BF-ZOU', country_code: 'BF', text: 'Zoundweogo' }, { value: 'BG-01', country_code: 'BG', text: 'Blagoevgrad' }, { value: 'BG-02', country_code: 'BG', text: 'Burgas' }, { value: 'BG-08', country_code: 'BG', text: 'Dobrich' }, { value: 'BG-07', country_code: 'BG', text: 'Gabrovo' }, { value: 'BG-26', country_code: 'BG', text: 'Haskovo' }, { value: 'BG-09', country_code: 'BG', text: 'Kardzhali' }, { value: 'BG-10', country_code: 'BG', text: 'Kyustendil' }, { value: 'BG-11', country_code: 'BG', text: 'Lovech' }, { value: 'BG-12', country_code: 'BG', text: 'Montana' }, { value: 'BG-13', country_code: 'BG', text: 'Pazardzhik' }, { value: 'BG-14', country_code: 'BG', text: 'Pernik' }, { value: 'BG-15', country_code: 'BG', text: 'Pleven' }, { value: 'BG-16', country_code: 'BG', text: 'Plovdiv' }, { value: 'BG-17', country_code: 'BG', text: 'Razgrad' }, { value: 'BG-18', country_code: 'BG', text: 'Ruse' }, { value: 'BG-27', country_code: 'BG', text: 'Shumen' }, { value: 'BG-19', country_code: 'BG', text: 'Silistra' }, { value: 'BG-20', country_code: 'BG', text: 'Sliven' }, { value: 'BG-21', country_code: 'BG', text: 'Smolyan' }, { value: 'BG-23', country_code: 'BG', text: 'Sofia' }, { value: 'BG-22', country_code: 'BG', text: 'Sofia (stolitsa)' }, { value: 'BG-24', country_code: 'BG', text: 'Stara Zagora' }, { value: 'BG-25', country_code: 'BG', text: 'Targovishte' }, { value: 'BG-03', country_code: 'BG', text: 'Varna' }, { value: 'BG-04', country_code: 'BG', text: 'Veliko Tarnovo' }, { value: 'BG-05', country_code: 'BG', text: 'Vidin' }, { value: 'BG-06', country_code: 'BG', text: 'Vratsa' }, { value: 'BG-28', country_code: 'BG', text: 'Yambol' }, { value: 'BH-13', country_code: 'BH', text: "Al 'Asimah" }, { value: 'BH-15', country_code: 'BH', text: 'Al Muharraq' }, { value: 'BH-17', country_code: 'BH', text: 'Ash Shamaliyah' }, { value: 'BI-BB', country_code: 'BI', text: 'Bubanza' }, { value: 'BI-BM', country_code: 'BI', text: 'Bujumbura Mairie' }, { value: 'BI-BR', country_code: 'BI', text: 'Bururi' }, { value: 'BI-CA', country_code: 'BI', text: 'Cankuzo' }, { value: 'BI-CI', country_code: 'BI', text: 'Cibitoke' }, { value: 'BI-GI', country_code: 'BI', text: 'Gitega' }, { value: 'BI-KR', country_code: 'BI', text: 'Karuzi' }, { value: 'BI-KY', country_code: 'BI', text: 'Kayanza' }, { value: 'BI-KI', country_code: 'BI', text: 'Kirundo' }, { value: 'BI-MA', country_code: 'BI', text: 'Makamba' }, { value: 'BI-MU', country_code: 'BI', text: 'Muramvya' }, { value: 'BI-MY', country_code: 'BI', text: 'Muyinga' }, { value: 'BI-MW', country_code: 'BI', text: 'Mwaro' }, { value: 'BI-NG', country_code: 'BI', text: 'Ngozi' }, { value: 'BI-RT', country_code: 'BI', text: 'Rutana' }, { value: 'BI-RY', country_code: 'BI', text: 'Ruyigi' }, { value: 'BJ-AL', country_code: 'BJ', text: 'Alibori' }, { value: 'BJ-AK', country_code: 'BJ', text: 'Atacora' }, { value: 'BJ-AQ', country_code: 'BJ', text: 'Atlantique' }, { value: 'BJ-BO', country_code: 'BJ', text: 'Borgou' }, { value: 'BJ-CO', country_code: 'BJ', text: 'Collines' }, { value: 'BJ-KO', country_code: 'BJ', text: 'Couffo' }, { value: 'BJ-DO', country_code: 'BJ', text: 'Donga' }, { value: 'BJ-LI', country_code: 'BJ', text: 'Littoral' }, { value: 'BJ-MO', country_code: 'BJ', text: 'Mono' }, { value: 'BJ-OU', country_code: 'BJ', text: 'Oueme' }, { value: 'BJ-PL', country_code: 'BJ', text: 'Plateau' }, { value: 'BJ-ZO', country_code: 'BJ', text: 'Zou' }, { value: 'BN-BE', country_code: 'BN', text: 'Belait' }, { value: 'BN-BM', country_code: 'BN', text: 'Brunei-Muara' }, { value: 'BN-TE', country_code: 'BN', text: 'Temburong' }, { value: 'BN-TU', country_code: 'BN', text: 'Tutong' }, { value: 'BO-H', country_code: 'BO', text: 'Chuquisaca' }, { value: 'BO-C', country_code: 'BO', text: 'Cochabamba' }, { value: 'BO-B', country_code: 'BO', text: 'El Beni' }, { value: 'BO-L', country_code: 'BO', text: 'La Paz' }, { value: 'BO-O', country_code: 'BO', text: 'Oruro' }, { value: 'BO-N', country_code: 'BO', text: 'Pando' }, { value: 'BO-P', country_code: 'BO', text: 'Potosi' }, { value: 'BO-S', country_code: 'BO', text: 'Santa Cruz' }, { value: 'BO-T', country_code: 'BO', text: 'Tarija' }, { value: 'BQ-BO', country_code: 'BQ', text: 'Bonaire' }, { value: 'BQ-SA', country_code: 'BQ', text: 'Saba' }, { value: 'BQ-SE', country_code: 'BQ', text: 'Sint Eustatius' }, { value: 'BR-AC', country_code: 'BR', text: 'Acre' }, { value: 'BR-AL', country_code: 'BR', text: 'Alagoas' }, { value: 'BR-AP', country_code: 'BR', text: 'Amapa' }, { value: 'BR-AM', country_code: 'BR', text: 'Amazonas' }, { value: 'BR-BA', country_code: 'BR', text: 'Bahia' }, { value: 'BR-CE', country_code: 'BR', text: 'Ceara' }, { value: 'BR-DF', country_code: 'BR', text: 'Distrito Federal' }, { value: 'BR-ES', country_code: 'BR', text: 'Espirito Santo' }, { value: 'BR-GO', country_code: 'BR', text: 'Goias' }, { value: 'BR-MA', country_code: 'BR', text: 'Maranhao' }, { value: 'BR-MT', country_code: 'BR', text: 'Mato Grosso' }, { value: 'BR-MS', country_code: 'BR', text: 'Mato Grosso do Sul' }, { value: 'BR-MG', country_code: 'BR', text: 'Minas Gerais' }, { value: 'BR-PA', country_code: 'BR', text: 'Para' }, { value: 'BR-PB', country_code: 'BR', text: 'Paraiba' }, { value: 'BR-PR', country_code: 'BR', text: 'Parana' }, { value: 'BR-PE', country_code: 'BR', text: 'Pernambuco' }, { value: 'BR-PI', country_code: 'BR', text: 'Piaui' }, { value: 'BR-RJ', country_code: 'BR', text: 'Rio de Janeiro' }, { value: 'BR-RN', country_code: 'BR', text: 'Rio Grande do Norte' }, { value: 'BR-RS', country_code: 'BR', text: 'Rio Grande do Sul' }, { value: 'BR-RO', country_code: 'BR', text: 'Rondonia' }, { value: 'BR-RR', country_code: 'BR', text: 'Roraima' }, { value: 'BR-SC', country_code: 'BR', text: 'Santa Catarina' }, { value: 'BR-SP', country_code: 'BR', text: 'Sao Paulo' }, { value: 'BR-SE', country_code: 'BR', text: 'Sergipe' }, { value: 'BR-TO', country_code: 'BR', text: 'Tocantins' }, { value: 'BS-CS', country_code: 'BS', text: 'Central Andros' }, { value: 'BS-FP', country_code: 'BS', text: 'City of Freeport' }, { value: 'BS-EG', country_code: 'BS', text: 'East Grand Bahama' }, { value: 'BS-HI', country_code: 'BS', text: 'Harbour Island' }, { value: 'BS-HT', country_code: 'BS', text: 'Hope Town' }, { value: 'BS-LI', country_code: 'BS', text: 'Long Island' }, { value: 'BS-SE', country_code: 'BS', text: 'South Eleuthera' }, { value: 'BT-12', country_code: 'BT', text: 'Chhukha' }, { value: 'BT-22', country_code: 'BT', text: 'Dagana' }, { value: 'BT-GA', country_code: 'BT', text: 'Gasa' }, { value: 'BT-13', country_code: 'BT', text: 'Haa' }, { value: 'BT-42', country_code: 'BT', text: 'Monggar' }, { value: 'BT-11', country_code: 'BT', text: 'Paro' }, { value: 'BT-23', country_code: 'BT', text: 'Punakha' }, { value: 'BT-15', country_code: 'BT', text: 'Thimphu' }, { value: 'BT-TY', country_code: 'BT', text: 'Trashi Yangtse' }, { value: 'BT-32', country_code: 'BT', text: 'Trongsa' }, { value: 'BT-34', country_code: 'BT', text: 'Zhemgang' }, { value: 'BW-CE', country_code: 'BW', text: 'Central' }, { value: 'BW-GH', country_code: 'BW', text: 'Ghanzi' }, { value: 'BW-KG', country_code: 'BW', text: 'Kgalagadi' }, { value: 'BW-KL', country_code: 'BW', text: 'Kgatleng' }, { value: 'BW-KW', country_code: 'BW', text: 'Kweneng' }, { value: 'BW-NE', country_code: 'BW', text: 'North East' }, { value: 'BW-NW', country_code: 'BW', text: 'North West' }, { value: 'BW-SE', country_code: 'BW', text: 'South East' }, { value: 'BW-SO', country_code: 'BW', text: 'Southern' }, { value: 'BY-BR', country_code: 'BY', text: "Brestskaya voblasts'" }, { value: 'BY-HO', country_code: 'BY', text: "Homyel'skaya voblasts'" }, { value: 'BY-HR', country_code: 'BY', text: "Hrodzenskaya voblasts'" }, { value: 'BY-MA', country_code: 'BY', text: "Mahilyowskaya voblasts'" }, { value: 'BY-MI', country_code: 'BY', text: "Minskaya voblasts'" }, { value: 'BY-VI', country_code: 'BY', text: "Vitsyebskaya voblasts'" }, { value: 'BZ-BZ', country_code: 'BZ', text: 'Belize' }, { value: 'BZ-CY', country_code: 'BZ', text: 'Cayo' }, { value: 'BZ-CZL', country_code: 'BZ', text: 'Corozal' }, { value: 'BZ-OW', country_code: 'BZ', text: 'Orange Walk' }, { value: 'BZ-SC', country_code: 'BZ', text: 'Stann Creek' }, { value: 'BZ-TOL', country_code: 'BZ', text: 'Toledo' }, { value: 'CA-AB', country_code: 'CA', text: 'Alberta' }, { value: 'CA-BC', country_code: 'CA', text: 'British Columbia' }, { value: 'CA-MB', country_code: 'CA', text: 'Manitoba' }, { value: 'CA-NB', country_code: 'CA', text: 'New Brunswick' }, { value: 'CA-NL', country_code: 'CA', text: 'Newfoundland and Labrador' }, { value: 'CA-NT', country_code: 'CA', text: 'Northwest Territories' }, { value: 'CA-NS', country_code: 'CA', text: 'Nova Scotia' }, { value: 'CA-NU', country_code: 'CA', text: 'Nunavut' }, { value: 'CA-ON', country_code: 'CA', text: 'Ontario' }, { value: 'CA-PE', country_code: 'CA', text: 'Prince Edward Island' }, { value: 'CA-QC', country_code: 'CA', text: 'Quebec' }, { value: 'CA-SK', country_code: 'CA', text: 'Saskatchewan' }, { value: 'CA-YT', country_code: 'CA', text: 'Yukon' }, { value: 'CD-BU', country_code: 'CD', text: 'Bas-Uele' }, { value: 'CD-EQ', country_code: 'CD', text: 'Equateur' }, { value: 'CD-HK', country_code: 'CD', text: 'Haut-Katanga' }, { value: 'CD-HL', country_code: 'CD', text: 'Haut-Lomani' }, { value: 'CD-HU', country_code: 'CD', text: 'Haut-Uele' }, { value: 'CD-IT', country_code: 'CD', text: 'Ituri' }, { value: 'CD-KS', country_code: 'CD', text: 'Kasai' }, { value: 'CD-KC', country_code: 'CD', text: 'Kasai Central' }, { value: 'CD-KE', country_code: 'CD', text: 'Kasai Oriental' }, { value: 'CD-KN', country_code: 'CD', text: 'Kinshasa' }, { value: 'CD-BC', country_code: 'CD', text: 'Kongo Central' }, { value: 'CD-KG', country_code: 'CD', text: 'Kwango' }, { value: 'CD-KL', country_code: 'CD', text: 'Kwilu' }, { value: 'CD-LO', country_code: 'CD', text: 'Lomami' }, { value: 'CD-LU', country_code: 'CD', text: 'Lualaba' }, { value: 'CD-MN', country_code: 'CD', text: 'Mai-Ndombe' }, { value: 'CD-MA', country_code: 'CD', text: 'Maniema' }, { value: 'CD-MO', country_code: 'CD', text: 'Mongala' }, { value: 'CD-NK', country_code: 'CD', text: 'Nord-Kivu' }, { value: 'CD-NU', country_code: 'CD', text: 'Nord-Ubangi' }, { value: 'CD-SA', country_code: 'CD', text: 'Sankuru' }, { value: 'CD-SK', country_code: 'CD', text: 'Sud-Kivu' }, { value: 'CD-SU', country_code: 'CD', text: 'Sud-Ubangi' }, { value: 'CD-TA', country_code: 'CD', text: 'Tanganyika' }, { value: 'CD-TO', country_code: 'CD', text: 'Tshopo' }, { value: 'CD-TU', country_code: 'CD', text: 'Tshuapa' }, { value: 'CF-BB', country_code: 'CF', text: 'Bamingui-Bangoran' }, { value: 'CF-BGF', country_code: 'CF', text: 'Bangui' }, { value: 'CF-BK', country_code: 'CF', text: 'Basse-Kotto' }, { value: 'CF-KB', country_code: 'CF', text: 'Gribingui' }, { value: 'CF-HM', country_code: 'CF', text: 'Haut-Mbomou' }, { value: 'CF-HK', country_code: 'CF', text: 'Haute-Kotto' }, { value: 'CF-KG', country_code: 'CF', text: 'Kemo-Gribingui' }, { value: 'CF-LB', country_code: 'CF', text: 'Lobaye' }, { value: 'CF-HS', country_code: 'CF', text: 'Mambere-Kadei' }, { value: 'CF-MB', country_code: 'CF', text: 'Mbomou' }, { value: 'CF-NM', country_code: 'CF', text: 'Nana-Mambere' }, { value: 'CF-MP', country_code: 'CF', text: 'Ombella-Mpoko' }, { value: 'CF-UK', country_code: 'CF', text: 'Ouaka' }, { value: 'CF-AC', country_code: 'CF', text: 'Ouham' }, { value: 'CF-OP', country_code: 'CF', text: 'Ouham-Pende' }, { value: 'CF-SE', country_code: 'CF', text: 'Sangha' }, { value: 'CG-11', country_code: 'CG', text: 'Bouenza' }, { value: 'CG-BZV', country_code: 'CG', text: 'Brazzaville' }, { value: 'CG-8', country_code: 'CG', text: 'Cuvette' }, { value: 'CG-15', country_code: 'CG', text: 'Cuvette-Ouest' }, { value: 'CG-2', country_code: 'CG', text: 'Lekoumou' }, { value: 'CG-7', country_code: 'CG', text: 'Likouala' }, { value: 'CG-9', country_code: 'CG', text: 'Niari' }, { value: 'CG-14', country_code: 'CG', text: 'Plateaux' }, { value: 'CG-16', country_code: 'CG', text: 'Pointe-Noire' }, { value: 'CG-12', country_code: 'CG', text: 'Pool' }, { value: 'CG-13', country_code: 'CG', text: 'Sangha' }, { value: 'CH-AG', country_code: 'CH', text: 'Aargau' }, { value: 'CH-AR', country_code: 'CH', text: 'Appenzell Ausserrhoden' }, { value: 'CH-AI', country_code: 'CH', text: 'Appenzell Innerrhoden' }, { value: 'CH-BL', country_code: 'CH', text: 'Basel-Landschaft' }, { value: 'CH-BS', country_code: 'CH', text: 'Basel-Stadt' }, { value: 'CH-BE', country_code: 'CH', text: 'Bern' }, { value: 'CH-FR', country_code: 'CH', text: 'Fribourg' }, { value: 'CH-GE', country_code: 'CH', text: 'Geneve' }, { value: 'CH-GL', country_code: 'CH', text: 'Glarus' }, { value: 'CH-GR', country_code: 'CH', text: 'Graubunden' }, { value: 'CH-JU', country_code: 'CH', text: 'Jura' }, { value: 'CH-LU', country_code: 'CH', text: 'Luzern' }, { value: 'CH-NE', country_code: 'CH', text: 'Neuchatel' }, { value: 'CH-NW', country_code: 'CH', text: 'Nidwalden' }, { value: 'CH-OW', country_code: 'CH', text: 'Obwalden' }, { value: 'CH-SG', country_code: 'CH', text: 'Sankt Gallen' }, { value: 'CH-SH', country_code: 'CH', text: 'Schaffhausen' }, { value: 'CH-SZ', country_code: 'CH', text: 'Schwyz' }, { value: 'CH-SO', country_code: 'CH', text: 'Solothurn' }, { value: 'CH-TG', country_code: 'CH', text: 'Thurgau' }, { value: 'CH-TI', country_code: 'CH', text: 'Ticino' }, { value: 'CH-UR', country_code: 'CH', text: 'Uri' }, { value: 'CH-VS', country_code: 'CH', text: 'Valais' }, { value: 'CH-VD', country_code: 'CH', text: 'Vaud' }, { value: 'CH-ZG', country_code: 'CH', text: 'Zug' }, { value: 'CH-ZH', country_code: 'CH', text: 'Zurich' }, { value: 'CI-AB', country_code: 'CI', text: 'Abidjan' }, { value: 'CI-BS', country_code: 'CI', text: 'Bas-Sassandra' }, { value: 'CI-CM', country_code: 'CI', text: 'Comoe' }, { value: 'CI-DN', country_code: 'CI', text: 'Denguele' }, { value: 'CI-GD', country_code: 'CI', text: 'Goh-Djiboua' }, { value: 'CI-LC', country_code: 'CI', text: 'Lacs' }, { value: 'CI-LG', country_code: 'CI', text: 'Lagunes' }, { value: 'CI-MG', country_code: 'CI', text: 'Montagnes' }, { value: 'CI-SM', country_code: 'CI', text: 'Sassandra-Marahoue' }, { value: 'CI-SV', country_code: 'CI', text: 'Savanes' }, { value: 'CI-VB', country_code: 'CI', text: 'Vallee du Bandama' }, { value: 'CI-WR', country_code: 'CI', text: 'Woroba' }, { value: 'CI-ZZ', country_code: 'CI', text: 'Zanzan' }, { value: 'CL-AI', country_code: 'CL', text: 'Aisen del General Carlos Ibanez del Campo' }, { value: 'CL-AN', country_code: 'CL', text: 'Antofagasta' }, { value: 'CL-AP', country_code: 'CL', text: 'Arica y Parinacota' }, { value: 'CL-AT', country_code: 'CL', text: 'Atacama' }, { value: 'CL-BI', country_code: 'CL', text: 'Biobio' }, { value: 'CL-CO', country_code: 'CL', text: 'Coquimbo' }, { value: 'CL-AR', country_code: 'CL', text: 'La Araucania' }, { value: 'CL-LI', country_code: 'CL', text: "Libertador General Bernardo O'Higgins" }, { value: 'CL-LL', country_code: 'CL', text: 'Los Lagos' }, { value: 'CL-LR', country_code: 'CL', text: 'Los Rios' }, { value: 'CL-MA', country_code: 'CL', text: 'Magallanes' }, { value: 'CL-ML', country_code: 'CL', text: 'Maule' }, { value: 'CL-RM', country_code: 'CL', text: 'Region Metropolitana de Santiago' }, { value: 'CL-TA', country_code: 'CL', text: 'Tarapaca' }, { value: 'CL-VS', country_code: 'CL', text: 'Valparaiso' }, { value: 'CM-AD', country_code: 'CM', text: 'Adamaoua' }, { value: 'CM-CE', country_code: 'CM', text: 'Centre' }, { value: 'CM-ES', country_code: 'CM', text: 'Est' }, { value: 'CM-EN', country_code: 'CM', text: 'Extreme-Nord' }, { value: 'CM-LT', country_code: 'CM', text: 'Littoral' }, { value: 'CM-NO', country_code: 'CM', text: 'Nord' }, { value: 'CM-NW', country_code: 'CM', text: 'Nord-Ouest' }, { value: 'CM-OU', country_code: 'CM', text: 'Ouest' }, { value: 'CM-SU', country_code: 'CM', text: 'Sud' }, { value: 'CM-SW', country_code: 'CM', text: 'Sud-Ouest' }, { value: 'CN-34', country_code: 'CN', text: 'Anhui' }, { value: 'CN-11', country_code: 'CN', text: 'Beijing' }, { value: 'CN-50', country_code: 'CN', text: 'Chongqing' }, { value: 'CN-35', country_code: 'CN', text: 'Fujian' }, { value: 'CN-62', country_code: 'CN', text: 'Gansu' }, { value: 'CN-44', country_code: 'CN', text: 'Guangdong' }, { value: 'CN-45', country_code: 'CN', text: 'Guangxi' }, { value: 'CN-52', country_code: 'CN', text: 'Guizhou' }, { value: 'CN-46', country_code: 'CN', text: 'Hainan' }, { value: 'CN-13', country_code: 'CN', text: 'Hebei' }, { value: 'CN-23', country_code: 'CN', text: 'Heilongjiang' }, { value: 'CN-41', country_code: 'CN', text: 'Henan' }, { value: 'CN-42', country_code: 'CN', text: 'Hubei' }, { value: 'CN-43', country_code: 'CN', text: 'Hunan' }, { value: 'CN-32', country_code: 'CN', text: 'Jiangsu' }, { value: 'CN-36', country_code: 'CN', text: 'Jiangxi' }, { value: 'CN-22', country_code: 'CN', text: 'Jilin' }, { value: 'CN-21', country_code: 'CN', text: 'Liaoning' }, { value: 'CN-15', country_code: 'CN', text: 'Nei Mongol' }, { value: 'CN-64', country_code: 'CN', text: 'Ningxia' }, { value: 'CN-63', country_code: 'CN', text: 'Qinghai' }, { value: 'CN-61', country_code: 'CN', text: 'Shaanxi' }, { value: 'CN-37', country_code: 'CN', text: 'Shandong' }, { value: 'CN-31', country_code: 'CN', text: 'Shanghai' }, { value: 'CN-14', country_code: 'CN', text: 'Shanxi' }, { value: 'CN-51', country_code: 'CN', text: 'Sichuan' }, { value: 'CN-12', country_code: 'CN', text: 'Tianjin' }, { value: 'CN-65', country_code: 'CN', text: 'Xinjiang' }, { value: 'CN-54', country_code: 'CN', text: 'Xizang' }, { value: 'CN-53', country_code: 'CN', text: 'Yunnan' }, { value: 'CN-33', country_code: 'CN', text: 'Zhejiang' }, { value: 'CO-AMA', country_code: 'CO', text: 'Amazonas' }, { value: 'CO-ANT', country_code: 'CO', text: 'Antioquia' }, { value: 'CO-ARA', country_code: 'CO', text: 'Arauca' }, { value: 'CO-ATL', country_code: 'CO', text: 'Atlantico' }, { value: 'CO-BOL', country_code: 'CO', text: 'Bolivar' }, { value: 'CO-BOY', country_code: 'CO', text: 'Boyaca' }, { value: 'CO-CAL', country_code: 'CO', text: 'Caldas' }, { value: 'CO-CAQ', country_code: 'CO', text: 'Caqueta' }, { value: 'CO-CAS', country_code: 'CO', text: 'Casanare' }, { value: 'CO-CAU', country_code: 'CO', text: 'Cauca' }, { value: 'CO-CES', country_code: 'CO', text: 'Cesar' }, { value: 'CO-CHO', country_code: 'CO', text: 'Choco' }, { value: 'CO-COR', country_code: 'CO', text: 'Cordoba' }, { value: 'CO-CUN', country_code: 'CO', text: 'Cundinamarca' }, { value: 'CO-DC', country_code: 'CO', text: 'Distrito Capital de Bogota' }, { value: 'CO-GUA', country_code: 'CO', text: 'Guainia' }, { value: 'CO-GUV', country_code: 'CO', text: 'Guaviare' }, { value: 'CO-HUI', country_code: 'CO', text: 'Huila' }, { value: 'CO-LAG', country_code: 'CO', text: 'La Guajira' }, { value: 'CO-MAG', country_code: 'CO', text: 'Magdalena' }, { value: 'CO-MET', country_code: 'CO', text: 'Meta' }, { value: 'CO-NAR', country_code: 'CO', text: 'Narino' }, { value: 'CO-NSA', country_code: 'CO', text: 'Norte de Santander' }, { value: 'CO-PUT', country_code: 'CO', text: 'Putumayo' }, { value: 'CO-QUI', country_code: 'CO', text: 'Quindio' }, { value: 'CO-RIS', country_code: 'CO', text: 'Risaralda' }, { value: 'CO-SAP', country_code: 'CO', text: 'San Andres, Providencia y Santa Catalina' }, { value: 'CO-SAN', country_code: 'CO', text: 'Santander' }, { value: 'CO-SUC', country_code: 'CO', text: 'Sucre' }, { value: 'CO-TOL', country_code: 'CO', text: 'Tolima' }, { value: 'CO-VAC', country_code: 'CO', text: 'Valle del Cauca' }, { value: 'CO-VAU', country_code: 'CO', text: 'Vaupes' }, { value: 'CO-VID', country_code: 'CO', text: 'Vichada' }, { value: 'CR-A', country_code: 'CR', text: 'Alajuela' }, { value: 'CR-C', country_code: 'CR', text: 'Cartago' }, { value: 'CR-G', country_code: 'CR', text: 'Guanacaste' }, { value: 'CR-H', country_code: 'CR', text: 'Heredia' }, { value: 'CR-L', country_code: 'CR', text: 'Limon' }, { value: 'CR-P', country_code: 'CR', text: 'Puntarenas' }, { value: 'CR-SJ', country_code: 'CR', text: 'San Jose' }, { value: 'CU-15', country_code: 'CU', text: 'Artemisa' }, { value: 'CU-09', country_code: 'CU', text: 'Camaguey' }, { value: 'CU-08', country_code: 'CU', text: 'Ciego de Avila' }, { value: 'CU-06', country_code: 'CU', text: 'Cienfuegos' }, { value: 'CU-12', country_code: 'CU', text: 'Granma' }, { value: 'CU-14', country_code: 'CU', text: 'Guantanamo' }, { value: 'CU-11', country_code: 'CU', text: 'Holguin' }, { value: 'CU-99', country_code: 'CU', text: 'Isla de la Juventud' }, { value: 'CU-03', country_code: 'CU', text: 'La Habana' }, { value: 'CU-10', country_code: 'CU', text: 'Las Tunas' }, { value: 'CU-04', country_code: 'CU', text: 'Matanzas' }, { value: 'CU-16', country_code: 'CU', text: 'Mayabeque' }, { value: 'CU-01', country_code: 'CU', text: 'Pinar del Rio' }, { value: 'CU-07', country_code: 'CU', text: 'Sancti Spiritus' }, { value: 'CU-13', country_code: 'CU', text: 'Santiago de Cuba' }, { value: 'CU-05', country_code: 'CU', text: 'Villa Clara' }, { value: 'CV-BV', country_code: 'CV', text: 'Boa Vista' }, { value: 'CV-BR', country_code: 'CV', text: 'Brava' }, { value: 'CV-MA', country_code: 'CV', text: 'Maio' }, { value: 'CV-MO', country_code: 'CV', text: 'Mosteiros' }, { value: 'CV-PA', country_code: 'CV', text: 'Paul' }, { value: 'CV-PN', country_code: 'CV', text: 'Porto Novo' }, { value: 'CV-PR', country_code: 'CV', text: 'Praia' }, { value: 'CV-RB', country_code: 'CV', text: 'Ribeira Brava' }, { value: 'CV-RG', country_code: 'CV', text: 'Ribeira Grande' }, { value: 'CV-RS', country_code: 'CV', text: 'Ribeira Grande de Santiago' }, { value: 'CV-SL', country_code: 'CV', text: 'Sal' }, { value: 'CV-CA', country_code: 'CV', text: 'Santa Catarina' }, { value: 'CV-CF', country_code: 'CV', text: 'Santa Catarina do Fogo' }, { value: 'CV-CR', country_code: 'CV', text: 'Santa Cruz' }, { value: 'CV-SD', country_code: 'CV', text: 'Sao Domingos' }, { value: 'CV-SF', country_code: 'CV', text: 'Sao Filipe' }, { value: 'CV-SM', country_code: 'CV', text: 'Sao Miguel' }, { value: 'CV-SS', country_code: 'CV', text: 'Sao Salvador do Mundo' }, { value: 'CV-SV', country_code: 'CV', text: 'Sao Vicente' }, { value: 'CV-TA', country_code: 'CV', text: 'Tarrafal' }, { value: 'CV-TS', country_code: 'CV', text: 'Tarrafal de Sao Nicolau' }, { value: 'CY-04', country_code: 'CY', text: 'Ammochostos' }, { value: 'CY-06', country_code: 'CY', text: 'Keryneia' }, { value: 'CY-03', country_code: 'CY', text: 'Larnaka' }, { value: 'CY-01', country_code: 'CY', text: 'Lefkosia' }, { value: 'CY-02', country_code: 'CY', text: 'Lemesos' }, { value: 'CY-05', country_code: 'CY', text: 'Pafos' }, { value: 'CZ-JC', country_code: 'CZ', text: 'Jihocesky kraj' }, { value: 'CZ-JM', country_code: 'CZ', text: 'Jihomoravsky kraj' }, { value: 'CZ-KA', country_code: 'CZ', text: 'Karlovarsky kraj' }, { value: 'CZ-63', country_code: 'CZ', text: 'Kraj Vysocina' }, { value: 'CZ-KR', country_code: 'CZ', text: 'Kralovehradecky kraj' }, { value: 'CZ-LI', country_code: 'CZ', text: 'Liberecky kraj' }, { value: 'CZ-MO', country_code: 'CZ', text: 'Moravskoslezsky kraj' }, { value: 'CZ-OL', country_code: 'CZ', text: 'Olomoucky kraj' }, { value: 'CZ-PA', country_code: 'CZ', text: 'Pardubicky kraj' }, { value: 'CZ-PL', country_code: 'CZ', text: 'Plzensky kraj' }, { value: 'CZ-10', country_code: 'CZ', text: 'Praha, Hlavni mesto' }, { value: 'CZ-ST', country_code: 'CZ', text: 'Stredocesky kraj' }, { value: 'CZ-US', country_code: 'CZ', text: 'Ustecky kraj' }, { value: 'CZ-ZL', country_code: 'CZ', text: 'Zlinsky kraj' }, { value: 'DE-BW', country_code: 'DE', text: 'Baden-Wurttemberg' }, { value: 'DE-BY', country_code: 'DE', text: 'Bayern' }, { value: 'DE-BE', country_code: 'DE', text: 'Berlin' }, { value: 'DE-BB', country_code: 'DE', text: 'Brandenburg' }, { value: 'DE-HB', country_code: 'DE', text: 'Bremen' }, { value: 'DE-HH', country_code: 'DE', text: 'Hamburg' }, { value: 'DE-HE', country_code: 'DE', text: 'Hessen' }, { value: 'DE-MV', country_code: 'DE', text: 'Mecklenburg-Vorpommern' }, { value: 'DE-NI', country_code: 'DE', text: 'Niedersachsen' }, { value: 'DE-NW', country_code: 'DE', text: 'Nordrhein-Westfalen' }, { value: 'DE-RP', country_code: 'DE', text: 'Rheinland-Pfalz' }, { value: 'DE-SL', country_code: 'DE', text: 'Saarland' }, { value: 'DE-SN', country_code: 'DE', text: 'Sachsen' }, { value: 'DE-ST', country_code: 'DE', text: 'Sachsen-Anhalt' }, { value: 'DE-SH', country_code: 'DE', text: 'Schleswig-Holstein' }, { value: 'DE-TH', country_code: 'DE', text: 'Thuringen' }, { value: 'DJ-AS', country_code: 'DJ', text: 'Ali Sabieh' }, { value: 'DJ-AR', country_code: 'DJ', text: 'Arta' }, { value: 'DJ-DI', country_code: 'DJ', text: 'Dikhil' }, { value: 'DJ-DJ', country_code: 'DJ', text: 'Djibouti' }, { value: 'DJ-OB', country_code: 'DJ', text: 'Obock' }, { value: 'DJ-TA', country_code: 'DJ', text: 'Tadjourah' }, { value: 'DK-84', country_code: 'DK', text: 'Hovedstaden' }, { value: 'DK-82', country_code: 'DK', text: 'Midtjylland' }, { value: 'DK-81', country_code: 'DK', text: 'Nordjylland' }, { value: 'DK-85', country_code: 'DK', text: 'Sjelland' }, { value: 'DK-83', country_code: 'DK', text: 'Syddanmark' }, { value: 'DM-02', country_code: 'DM', text: 'Saint Andrew' }, { value: 'DM-03', country_code: 'DM', text: 'Saint David' }, { value: 'DM-04', country_code: 'DM', text: 'Saint George' }, { value: 'DM-05', country_code: 'DM', text: 'Saint John' }, { value: 'DM-06', country_code: 'DM', text: 'Saint Joseph' }, { value: 'DM-07', country_code: 'DM', text: 'Saint Luke' }, { value: 'DM-08', country_code: 'DM', text: 'Saint Mark' }, { value: 'DM-09', country_code: 'DM', text: 'Saint Patrick' }, { value: 'DM-10', country_code: 'DM', text: 'Saint Paul' }, { value: 'DO-02', country_code: 'DO', text: 'Azua' }, { value: 'DO-03', country_code: 'DO', text: 'Baoruco' }, { value: 'DO-04', country_code: 'DO', text: 'Barahona' }, { value: 'DO-05', country_code: 'DO', text: 'Dajabon' }, { value: 'DO-01', country_code: 'DO', text: 'Distrito Nacional (Santo Domingo)' }, { value: 'DO-06', country_code: 'DO', text: 'Duarte' }, { value: 'DO-08', country_code: 'DO', text: 'El Seibo' }, { value: 'DO-07', country_code: 'DO', text: 'Elias Pina' }, { value: 'DO-09', country_code: 'DO', text: 'Espaillat' }, { value: 'DO-30', country_code: 'DO', text: 'Hato Mayor' }, { value: 'DO-19', country_code: 'DO', text: 'Hermanas Mirabal' }, { value: 'DO-10', country_code: 'DO', text: 'Independencia' }, { value: 'DO-11', country_code: 'DO', text: 'La Altagracia' }, { value: 'DO-12', country_code: 'DO', text: 'La Romana' }, { value: 'DO-13', country_code: 'DO', text: 'La Vega' }, { value: 'DO-14', country_code: 'DO', text: 'Maria Trinidad Sanchez' }, { value: 'DO-28', country_code: 'DO', text: 'Monsenor Nouel' }, { value: 'DO-15', country_code: 'DO', text: 'Monte Cristi' }, { value: 'DO-29', country_code: 'DO', text: 'Monte Plata' }, { value: 'DO-16', country_code: 'DO', text: 'Pedernales' }, { value: 'DO-17', country_code: 'DO', text: 'Peravia' }, { value: 'DO-18', country_code: 'DO', text: 'Puerto Plata' }, { value: 'DO-20', country_code: 'DO', text: 'Samana' }, { value: 'DO-21', country_code: 'DO', text: 'San Cristobal' }, { value: 'DO-22', country_code: 'DO', text: 'San Juan' }, { value: 'DO-23', country_code: 'DO', text: 'San Pedro de Macoris' }, { value: 'DO-24', country_code: 'DO', text: 'Sanchez Ramirez' }, { value: 'DO-25', country_code: 'DO', text: 'Santiago' }, { value: 'DO-26', country_code: 'DO', text: 'Santiago Rodriguez' }, { value: 'DO-27', country_code: 'DO', text: 'Valverde' }, { value: 'DZ-01', country_code: 'DZ', text: 'Adrar' }, { value: 'DZ-44', country_code: 'DZ', text: 'Ain Defla' }, { value: 'DZ-46', country_code: 'DZ', text: 'Ain Temouchent' }, { value: 'DZ-16', country_code: 'DZ', text: 'Alger' }, { value: 'DZ-23', country_code: 'DZ', text: 'Annaba' }, { value: 'DZ-05', country_code: 'DZ', text: 'Batna' }, { value: 'DZ-08', country_code: 'DZ', text: 'Bechar' }, { value: 'DZ-06', country_code: 'DZ', text: 'Bejaia' }, { value: 'DZ-07', country_code: 'DZ', text: 'Biskra' }, { value: 'DZ-09', country_code: 'DZ', text: 'Blida' }, { value: 'DZ-34', country_code: 'DZ', text: 'Bordj Bou Arreridj' }, { value: 'DZ-10', country_code: 'DZ', text: 'Bouira' }, { value: 'DZ-35', country_code: 'DZ', text: 'Boumerdes' }, { value: 'DZ-02', country_code: 'DZ', text: 'Chlef' }, { value: 'DZ-25', country_code: 'DZ', text: 'Constantine' }, { value: 'DZ-17', country_code: 'DZ', text: 'Djelfa' }, { value: 'DZ-32', country_code: 'DZ', text: 'El Bayadh' }, { value: 'DZ-39', country_code: 'DZ', text: 'El Oued' }, { value: 'DZ-36', country_code: 'DZ', text: 'El Tarf' }, { value: 'DZ-47', country_code: 'DZ', text: 'Ghardaia' }, { value: 'DZ-24', country_code: 'DZ', text: 'Guelma' }, { value: 'DZ-33', country_code: 'DZ', text: 'Illizi' }, { value: 'DZ-40', country_code: 'DZ', text: 'Khenchela' }, { value: 'DZ-03', country_code: 'DZ', text: 'Laghouat' }, { value: 'DZ-28', country_code: 'DZ', text: "M'sila" }, { value: 'DZ-29', country_code: 'DZ', text: 'Mascara' }, { value: 'DZ-26', country_code: 'DZ', text: 'Medea' }, { value: 'DZ-43', country_code: 'DZ', text: 'Mila' }, { value: 'DZ-27', country_code: 'DZ', text: 'Mostaganem' }, { value: 'DZ-45', country_code: 'DZ', text: 'Naama' }, { value: 'DZ-31', country_code: 'DZ', text: 'Oran' }, { value: 'DZ-30', country_code: 'DZ', text: 'Ouargla' }, { value: 'DZ-04', country_code: 'DZ', text: 'Oum el Bouaghi' }, { value: 'DZ-48', country_code: 'DZ', text: 'Relizane' }, { value: 'DZ-20', country_code: 'DZ', text: 'Saida' }, { value: 'DZ-19', country_code: 'DZ', text: 'Setif' }, { value: 'DZ-22', country_code: 'DZ', text: 'Sidi Bel Abbes' }, { value: 'DZ-21', country_code: 'DZ', text: 'Skikda' }, { value: 'DZ-41', country_code: 'DZ', text: 'Souk Ahras' }, { value: 'DZ-11', country_code: 'DZ', text: 'Tamanrasset' }, { value: 'DZ-12', country_code: 'DZ', text: 'Tebessa' }, { value: 'DZ-14', country_code: 'DZ', text: 'Tiaret' }, { value: 'DZ-37', country_code: 'DZ', text: 'Tindouf' }, { value: 'DZ-42', country_code: 'DZ', text: 'Tipaza' }, { value: 'DZ-38', country_code: 'DZ', text: 'Tissemsilt' }, { value: 'DZ-15', country_code: 'DZ', text: 'Tizi Ouzou' }, { value: 'DZ-13', country_code: 'DZ', text: 'Tlemcen' }, { value: 'EC-A', country_code: 'EC', text: 'Azuay' }, { value: 'EC-B', country_code: 'EC', text: 'Bolivar' }, { value: 'EC-F', country_code: 'EC', text: 'Canar' }, { value: 'EC-C', country_code: 'EC', text: 'Carchi' }, { value: 'EC-H', country_code: 'EC', text: 'Chimborazo' }, { value: 'EC-X', country_code: 'EC', text: 'Cotopaxi' }, { value: 'EC-O', country_code: 'EC', text: 'El Oro' }, { value: 'EC-E', country_code: 'EC', text: 'Esmeraldas' }, { value: 'EC-W', country_code: 'EC', text: 'Galapagos' }, { value: 'EC-G', country_code: 'EC', text: 'Guayas' }, { value: 'EC-I', country_code: 'EC', text: 'Imbabura' }, { value: 'EC-L', country_code: 'EC', text: 'Loja' }, { value: 'EC-R', country_code: 'EC', text: 'Los Rios' }, { value: 'EC-M', country_code: 'EC', text: 'Manabi' }, { value: 'EC-S', country_code: 'EC', text: 'Morona-Santiago' }, { value: 'EC-N', country_code: 'EC', text: 'Napo' }, { value: 'EC-D', country_code: 'EC', text: 'Orellana' }, { value: 'EC-Y', country_code: 'EC', text: 'Pastaza' }, { value: 'EC-P', country_code: 'EC', text: 'Pichincha' }, { value: 'EC-SE', country_code: 'EC', text: 'Santa Elena' }, { value: 'EC-U', country_code: 'EC', text: 'Sucumbios' }, { value: 'EC-T', country_code: 'EC', text: 'Tungurahua' }, { value: 'EC-Z', country_code: 'EC', text: 'Zamora-Chinchipe' }, { value: 'EE-37', country_code: 'EE', text: 'Harjumaa' }, { value: 'EE-39', country_code: 'EE', text: 'Hiiumaa' }, { value: 'EE-44', country_code: 'EE', text: 'Ida-Virumaa' }, { value: 'EE-51', country_code: 'EE', text: 'Jarvamaa' }, { value: 'EE-49', country_code: 'EE', text: 'Jogevamaa' }, { value: 'EE-59', country_code: 'EE', text: 'Laane-Virumaa' }, { value: 'EE-57', country_code: 'EE', text: 'Laanemaa' }, { value: 'EE-67', country_code: 'EE', text: 'Parnumaa' }, { value: 'EE-65', country_code: 'EE', text: 'Polvamaa' }, { value: 'EE-70', country_code: 'EE', text: 'Raplamaa' }, { value: 'EE-74', country_code: 'EE', text: 'Saaremaa' }, { value: 'EE-78', country_code: 'EE', text: 'Tartumaa' }, { value: 'EE-82', country_code: 'EE', text: 'Valgamaa' }, { value: 'EE-84', country_code: 'EE', text: 'Viljandimaa' }, { value: 'EE-86', country_code: 'EE', text: 'Vorumaa' }, { value: 'EG-DK', country_code: 'EG', text: 'Ad Daqahliyah' }, { value: 'EG-BA', country_code: 'EG', text: 'Al Bahr al Ahmar' }, { value: 'EG-BH', country_code: 'EG', text: 'Al Buhayrah' }, { value: 'EG-FYM', country_code: 'EG', text: 'Al Fayyum' }, { value: 'EG-GH', country_code: 'EG', text: 'Al Gharbiyah' }, { value: 'EG-ALX', country_code: 'EG', text: 'Al Iskandariyah' }, { value: 'EG-IS', country_code: 'EG', text: "Al Isma'iliyah" }, { value: 'EG-GZ', country_code: 'EG', text: 'Al Jizah' }, { value: 'EG-MNF', country_code: 'EG', text: 'Al Minufiyah' }, { value: 'EG-MN', country_code: 'EG', text: 'Al Minya' }, { value: 'EG-C', country_code: 'EG', text: 'Al Qahirah' }, { value: 'EG-KB', country_code: 'EG', text: 'Al Qalyubiyah' }, { value: 'EG-LX', country_code: 'EG', text: 'Al Uqsur' }, { value: 'EG-WAD', country_code: 'EG', text: 'Al Wadi al Jadid' }, { value: 'EG-SUZ', country_code: 'EG', text: 'As Suways' }, { value: 'EG-SHR', country_code: 'EG', text: 'Ash Sharqiyah' }, { value: 'EG-ASN', country_code: 'EG', text: 'Aswan' }, { value: 'EG-AST', country_code: 'EG', text: 'Asyut' }, { value: 'EG-BNS', country_code: 'EG', text: 'Bani Suwayf' }, { value: 'EG-PTS', country_code: 'EG', text: "Bur Sa'id" }, { value: 'EG-DT', country_code: 'EG', text: 'Dumyat' }, { value: 'EG-JS', country_code: 'EG', text: "Janub Sina'" }, { value: 'EG-KFS', country_code: 'EG', text: 'Kafr ash Shaykh' }, { value: 'EG-MT', country_code: 'EG', text: 'Matruh' }, { value: 'EG-KN', country_code: 'EG', text: 'Qina' }, { value: 'EG-SIN', country_code: 'EG', text: "Shamal Sina'" }, { value: 'EG-SHG', country_code: 'EG', text: 'Suhaj' }, { value: 'ER-MA', country_code: 'ER', text: 'Al Awsat' }, { value: 'ER-DU', country_code: 'ER', text: 'Al Janubi' }, { value: 'ER-AN', country_code: 'ER', text: 'Ansaba' }, { value: 'ER-DK', country_code: 'ER', text: 'Janubi al Bahri al Ahmar' }, { value: 'ER-GB', country_code: 'ER', text: 'Qash-Barkah' }, { value: 'ER-SK', country_code: 'ER', text: 'Shimali al Bahri al Ahmar' }, { value: 'ES-AN', country_code: 'ES', text: 'Andalucia' }, { value: 'ES-AR', country_code: 'ES', text: 'Aragon' }, { value: 'ES-AS', country_code: 'ES', text: 'Asturias, Principado de' }, { value: 'ES-CN', country_code: 'ES', text: 'Canarias' }, { value: 'ES-CB', country_code: 'ES', text: 'Cantabria' }, { value: 'ES-CL', country_code: 'ES', text: 'Castilla y Leon' }, { value: 'ES-CM', country_code: 'ES', text: 'Castilla-La Mancha' }, { value: 'ES-CT', country_code: 'ES', text: 'Catalunya' }, { value: 'ES-CE', country_code: 'ES', text: 'Ceuta' }, { value: 'ES-EX', country_code: 'ES', text: 'Extremadura' }, { value: 'ES-GA', country_code: 'ES', text: 'Galicia' }, { value: 'ES-IB', country_code: 'ES', text: 'Illes Balears' }, { value: 'ES-RI', country_code: 'ES', text: 'La Rioja' }, { value: 'ES-MD', country_code: 'ES', text: 'Madrid, Comunidad de' }, { value: 'ES-ML', country_code: 'ES', text: 'Melilla' }, { value: 'ES-MC', country_code: 'ES', text: 'Murcia, Region de' }, { value: 'ES-NC', country_code: 'ES', text: 'Navarra, Comunidad Foral de' }, { value: 'ES-PV', country_code: 'ES', text: 'Pais Vasco' }, { value: 'ES-VC', country_code: 'ES', text: 'Valenciana, Comunidad' }, { value: 'ET-AA', country_code: 'ET', text: 'Adis Abeba' }, { value: 'ET-AF', country_code: 'ET', text: 'Afar' }, { value: 'ET-AM', country_code: 'ET', text: 'Amara' }, { value: 'ET-BE', country_code: 'ET', text: 'Binshangul Gumuz' }, { value: 'ET-DD', country_code: 'ET', text: 'Dire Dawa' }, { value: 'ET-GA', country_code: 'ET', text: 'Gambela Hizboch' }, { value: 'ET-HA', country_code: 'ET', text: 'Hareri Hizb' }, { value: 'ET-OR', country_code: 'ET', text: 'Oromiya' }, { value: 'ET-SO', country_code: 'ET', text: 'Sumale' }, { value: 'ET-TI', country_code: 'ET', text: 'Tigray' }, { value: 'ET-SN', country_code: 'ET', text: 'YeDebub Biheroch Bihereseboch na Hizboch' }, { value: 'FI-02', country_code: 'FI', text: 'Etela-Karjala' }, { value: 'FI-03', country_code: 'FI', text: 'Etela-Pohjanmaa' }, { value: 'FI-04', country_code: 'FI', text: 'Etela-Savo' }, { value: 'FI-05', country_code: 'FI', text: 'Kainuu' }, { value: 'FI-06', country_code: 'FI', text: 'Kanta-Hame' }, { value: 'FI-07', country_code: 'FI', text: 'Keski-Pohjanmaa' }, { value: 'FI-08', country_code: 'FI', text: 'Keski-Suomi' }, { value: 'FI-09', country_code: 'FI', text: 'Kymenlaakso' }, { value: 'FI-10', country_code: 'FI', text: 'Lappi' }, { value: 'FI-16', country_code: 'FI', text: 'Paijat-Hame' }, { value: 'FI-11', country_code: 'FI', text: 'Pirkanmaa' }, { value: 'FI-12', country_code: 'FI', text: 'Pohjanmaa' }, { value: 'FI-13', country_code: 'FI', text: 'Pohjois-Karjala' }, { value: 'FI-14', country_code: 'FI', text: 'Pohjois-Pohjanmaa' }, { value: 'FI-15', country_code: 'FI', text: 'Pohjois-Savo' }, { value: 'FI-17', country_code: 'FI', text: 'Satakunta' }, { value: 'FI-18', country_code: 'FI', text: 'Uusimaa' }, { value: 'FI-19', country_code: 'FI', text: 'Varsinais-Suomi' }, { value: 'FJ-C', country_code: 'FJ', text: 'Central' }, { value: 'FJ-N', country_code: 'FJ', text: 'Northern' }, { value: 'FJ-W', country_code: 'FJ', text: 'Western' }, { value: 'FM-TRK', country_code: 'FM', text: 'Chuuk' }, { value: 'FM-KSA', country_code: 'FM', text: 'Kosrae' }, { value: 'FM-PNI', country_code: 'FM', text: 'Pohnpei' }, { value: 'FM-YAP', country_code: 'FM', text: 'Yap' }, { value: 'FR-ARA', country_code: 'FR', text: 'Auvergne-Rhone-Alpes' }, { value: 'FR-BFC', country_code: 'FR', text: 'Bourgogne-Franche-Comte' }, { value: 'FR-E', country_code: 'FR', text: 'Bretagne' }, { value: 'FR-CVL', country_code: 'FR', text: 'Centre-Val de Loire' }, { value: 'FR-H', country_code: 'FR', text: 'Corse' }, { value: 'FR-GES', country_code: 'FR', text: 'Grand-Est' }, { value: 'FR-HDF', country_code: 'FR', text: 'Hauts-de-France' }, { value: 'FR-J', country_code: 'FR', text: 'Ile-de-France' }, { value: 'FR-NOR', country_code: 'FR', text: 'Normandie' }, { value: 'FR-NAQ', country_code: 'FR', text: 'Nouvelle-Aquitaine' }, { value: 'FR-OCC', country_code: 'FR', text: 'Occitanie' }, { value: 'FR-R', country_code: 'FR', text: 'Pays-de-la-Loire' }, { value: 'FR-PAC', country_code: 'FR', text: "Provence-Alpes-Cote d'Azur" }, { value: 'GA-1', country_code: 'GA', text: 'Estuaire' }, { value: 'GA-2', country_code: 'GA', text: 'Haut-Ogooue' }, { value: 'GA-3', country_code: 'GA', text: 'Moyen-Ogooue' }, { value: 'GA-4', country_code: 'GA', text: 'Ngounie' }, { value: 'GA-5', country_code: 'GA', text: 'Nyanga' }, { value: 'GA-6', country_code: 'GA', text: 'Ogooue-Ivindo' }, { value: 'GA-7', country_code: 'GA', text: 'Ogooue-Lolo' }, { value: 'GA-8', country_code: 'GA', text: 'Ogooue-Maritime' }, { value: 'GA-9', country_code: 'GA', text: 'Woleu-Ntem' }, { value: 'GB-ENG', country_code: 'GB', text: 'England' }, { value: 'GB-NIR', country_code: 'GB', text: 'Northern Ireland' }, { value: 'GB-SCT', country_code: 'GB', text: 'Scotland' }, { value: 'GB-WLS', country_code: 'GB', text: 'Wales' }, { value: 'GD-01', country_code: 'GD', text: 'Saint Andrew' }, { value: 'GD-02', country_code: 'GD', text: 'Saint David' }, { value: 'GD-03', country_code: 'GD', text: 'Saint George' }, { value: 'GD-04', country_code: 'GD', text: 'Saint John' }, { value: 'GD-05', country_code: 'GD', text: 'Saint Mark' }, { value: 'GD-06', country_code: 'GD', text: 'Saint Patrick' }, { value: 'GE-AB', country_code: 'GE', text: 'Abkhazia' }, { value: 'GE-AJ', country_code: 'GE', text: 'Ajaria' }, { value: 'GE-GU', country_code: 'GE', text: 'Guria' }, { value: 'GE-IM', country_code: 'GE', text: 'Imereti' }, { value: 'GE-KA', country_code: 'GE', text: "K'akheti" }, { value: 'GE-KK', country_code: 'GE', text: 'Kvemo Kartli' }, { value: 'GE-MM', country_code: 'GE', text: 'Mtskheta-Mtianeti' }, { value: 'GE-RL', country_code: 'GE', text: "Rach'a-Lechkhumi-Kvemo Svaneti" }, { value: 'GE-SZ', country_code: 'GE', text: 'Samegrelo-Zemo Svaneti' }, { value: 'GE-SJ', country_code: 'GE', text: 'Samtskhe-Javakheti' }, { value: 'GE-SK', country_code: 'GE', text: 'Shida Kartli' }, { value: 'GE-TB', country_code: 'GE', text: 'Tbilisi' }, { value: 'GH-AH', country_code: 'GH', text: 'Ashanti' }, { value: 'GH-BA', country_code: 'GH', text: 'Brong-Ahafo' }, { value: 'GH-CP', country_code: 'GH', text: 'Central' }, { value: 'GH-EP', country_code: 'GH', text: 'Eastern' }, { value: 'GH-AA', country_code: 'GH', text: 'Greater Accra' }, { value: 'GH-NP', country_code: 'GH', text: 'Northern' }, { value: 'GH-UE', country_code: 'GH', text: 'Upper East' }, { value: 'GH-UW', country_code: 'GH', text: 'Upper West' }, { value: 'GH-TV', country_code: 'GH', text: 'Volta' }, { value: 'GH-WP', country_code: 'GH', text: 'Western' }, { value: 'GL-KU', country_code: 'GL', text: 'Kommune Kujalleq' }, { value: 'GL-SM', country_code: 'GL', text: 'Kommuneqarfik Sermersooq' }, { value: 'GL-QA', country_code: 'GL', text: 'Qaasuitsup Kommunia' }, { value: 'GL-QE', country_code: 'GL', text: 'Qeqqata Kommunia' }, { value: 'GM-B', country_code: 'GM', text: 'Banjul' }, { value: 'GM-M', country_code: 'GM', text: 'Central River' }, { value: 'GM-L', country_code: 'GM', text: 'Lower River' }, { value: 'GM-N', country_code: 'GM', text: 'North Bank' }, { value: 'GM-U', country_code: 'GM', text: 'Upper River' }, { value: 'GM-W', country_code: 'GM', text: 'Western' }, { value: 'GN-BE', country_code: 'GN', text: 'Beyla' }, { value: 'GN-BF', country_code: 'GN', text: 'Boffa' }, { value: 'GN-B', country_code: 'GN', text: 'Boke' }, { value: 'GN-C', country_code: 'GN', text: 'Conakry' }, { value: 'GN-CO', country_code: 'GN', text: 'Coyah' }, { value: 'GN-DB', country_code: 'GN', text: 'Dabola' }, { value: 'GN-DL', country_code: 'GN', text: 'Dalaba' }, { value: 'GN-DI', country_code: 'GN', text: 'Dinguiraye' }, { value: 'GN-DU', country_code: 'GN', text: 'Dubreka' }, { value: 'GN-F', country_code: 'GN', text: 'Faranah' }, { value: 'GN-FO', country_code: 'GN', text: 'Forecariah' }, { value: 'GN-FR', country_code: 'GN', text: 'Fria' }, { value: 'GN-GA', country_code: 'GN', text: 'Gaoual' }, { value: 'GN-GU', country_code: 'GN', text: 'Guekedou' }, { value: 'GN-K', country_code: 'GN', text: 'Kankan' }, { value: 'GN-KE', country_code: 'GN', text: 'Kerouane' }, { value: 'GN-D', country_code: 'GN', text: 'Kindia' }, { value: 'GN-KS', country_code: 'GN', text: 'Kissidougou' }, { value: 'GN-KB', country_code: 'GN', text: 'Koubia' }, { value: 'GN-KN', country_code: 'GN', text: 'Koundara' }, { value: 'GN-KO', country_code: 'GN', text: 'Kouroussa' }, { value: 'GN-L', country_code: 'GN', text: 'Labe' }, { value: 'GN-LE', country_code: 'GN', text: 'Lelouma' }, { value: 'GN-LO', country_code: 'GN', text: 'Lola' }, { value: 'GN-MC', country_code: 'GN', text: 'Macenta' }, { value: 'GN-ML', country_code: 'GN', text: 'Mali' }, { value: 'GN-M', country_code: 'GN', text: 'Mamou' }, { value: 'GN-MD', country_code: 'GN', text: 'Mandiana' }, { value: 'GN-N', country_code: 'GN', text: 'Nzerekore' }, { value: 'GN-PI', country_code: 'GN', text: 'Pita' }, { value: 'GN-SI', country_code: 'GN', text: 'Siguiri' }, { value: 'GN-TE', country_code: 'GN', text: 'Telimele' }, { value: 'GN-TO', country_code: 'GN', text: 'Tougue' }, { value: 'GN-YO', country_code: 'GN', text: 'Yomou' }, { value: 'GQ-AN', country_code: 'GQ', text: 'Annobon' }, { value: 'GQ-BN', country_code: 'GQ', text: 'Bioko Norte' }, { value: 'GQ-BS', country_code: 'GQ', text: 'Bioko Sur' }, { value: 'GQ-CS', country_code: 'GQ', text: 'Centro Sur' }, { value: 'GQ-KN', country_code: 'GQ', text: 'Kie-Ntem' }, { value: 'GQ-LI', country_code: 'GQ', text: 'Litoral' }, { value: 'GQ-WN', country_code: 'GQ', text: 'Wele-Nzas' }, { value: 'GR-A', country_code: 'GR', text: 'Anatoliki Makedonia kai Thraki' }, { value: 'GR-I', country_code: 'GR', text: 'Attiki' }, { value: 'GR-G', country_code: 'GR', text: 'Dytiki Ellada' }, { value: 'GR-C', country_code: 'GR', text: 'Dytiki Makedonia' }, { value: 'GR-F', country_code: 'GR', text: 'Ionia Nisia' }, { value: 'GR-D', country_code: 'GR', text: 'Ipeiros' }, { value: 'GR-B', country_code: 'GR', text: 'Kentriki Makedonia' }, { value: 'GR-M', country_code: 'GR', text: 'Kriti' }, { value: 'GR-L', country_code: 'GR', text: 'Notio Aigaio' }, { value: 'GR-J', country_code: 'GR', text: 'Peloponnisos' }, { value: 'GR-H', country_code: 'GR', text: 'Sterea Ellada' }, { value: 'GR-E', country_code: 'GR', text: 'Thessalia' }, { value: 'GR-K', country_code: 'GR', text: 'Voreio Aigaio' }, { value: 'GT-AV', country_code: 'GT', text: 'Alta Verapaz' }, { value: 'GT-BV', country_code: 'GT', text: 'Baja Verapaz' }, { value: 'GT-CM', country_code: 'GT', text: 'Chimaltenango' }, { value: 'GT-CQ', country_code: 'GT', text: 'Chiquimula' }, { value: 'GT-PR', country_code: 'GT', text: 'El Progreso' }, { value: 'GT-ES', country_code: 'GT', text: 'Escuintla' }, { value: 'GT-GU', country_code: 'GT', text: 'Guatemala' }, { value: 'GT-HU', country_code: 'GT', text: 'Huehuetenango' }, { value: 'GT-IZ', country_code: 'GT', text: 'Izabal' }, { value: 'GT-JA', country_code: 'GT', text: 'Jalapa' }, { value: 'GT-JU', country_code: 'GT', text: 'Jutiapa' }, { value: 'GT-PE', country_code: 'GT', text: 'Peten' }, { value: 'GT-QZ', country_code: 'GT', text: 'Quetzaltenango' }, { value: 'GT-QC', country_code: 'GT', text: 'Quiche' }, { value: 'GT-RE', country_code: 'GT', text: 'Retalhuleu' }, { value: 'GT-SA', country_code: 'GT', text: 'Sacatepequez' }, { value: 'GT-SM', country_code: 'GT', text: 'San Marcos' }, { value: 'GT-SR', country_code: 'GT', text: 'Santa Rosa' }, { value: 'GT-SO', country_code: 'GT', text: 'Solola' }, { value: 'GT-SU', country_code: 'GT', text: 'Suchitepequez' }, { value: 'GT-TO', country_code: 'GT', text: 'Totonicapan' }, { value: 'GT-ZA', country_code: 'GT', text: 'Zacapa' }, { value: 'GW-BA', country_code: 'GW', text: 'Bafata' }, { value: 'GW-BM', country_code: 'GW', text: 'Biombo' }, { value: 'GW-BS', country_code: 'GW', text: 'Bissau' }, { value: 'GW-BL', country_code: 'GW', text: 'Bolama' }, { value: 'GW-CA', country_code: 'GW', text: 'Cacheu' }, { value: 'GW-GA', country_code: 'GW', text: 'Gabu' }, { value: 'GW-OI', country_code: 'GW', text: 'Oio' }, { value: 'GW-QU', country_code: 'GW', text: 'Quinara' }, { value: 'GW-TO', country_code: 'GW', text: 'Tombali' }, { value: 'GY-CU', country_code: 'GY', text: 'Cuyuni-Mazaruni' }, { value: 'GY-DE', country_code: 'GY', text: 'Demerara-Mahaica' }, { value: 'GY-EB', country_code: 'GY', text: 'East Berbice-Corentyne' }, { value: 'GY-ES', country_code: 'GY', text: 'Essequibo Islands-West Demerara' }, { value: 'GY-MA', country_code: 'GY', text: 'Mahaica-Berbice' }, { value: 'GY-PM', country_code: 'GY', text: 'Pomeroon-Supenaam' }, { value: 'GY-UD', country_code: 'GY', text: 'Upper Demerara-Berbice' }, { value: 'HN-AT', country_code: 'HN', text: 'Atlantida' }, { value: 'HN-CH', country_code: 'HN', text: 'Choluteca' }, { value: 'HN-CL', country_code: 'HN', text: 'Colon' }, { value: 'HN-CM', country_code: 'HN', text: 'Comayagua' }, { value: 'HN-CP', country_code: 'HN', text: 'Copan' }, { value: 'HN-CR', country_code: 'HN', text: 'Cortes' }, { value: 'HN-EP', country_code: 'HN', text: 'El Paraiso' }, { value: 'HN-FM', country_code: 'HN', text: 'Francisco Morazan' }, { value: 'HN-GD', country_code: 'HN', text: 'Gracias a Dios' }, { value: 'HN-IN', country_code: 'HN', text: 'Intibuca' }, { value: 'HN-IB', country_code: 'HN', text: 'Islas de la Bahia' }, { value: 'HN-LP', country_code: 'HN', text: 'La Paz' }, { value: 'HN-LE', country_code: 'HN', text: 'Lempira' }, { value: 'HN-OC', country_code: 'HN', text: 'Ocotepeque' }, { value: 'HN-OL', country_code: 'HN', text: 'Olancho' }, { value: 'HN-SB', country_code: 'HN', text: 'Santa Barbara' }, { value: 'HN-VA', country_code: 'HN', text: 'Valle' }, { value: 'HN-YO', country_code: 'HN', text: 'Yoro' }, { value: 'HR-07', country_code: 'HR', text: 'Bjelovarsko-bilogorska zupanija' }, { value: 'HR-12', country_code: 'HR', text: 'Brodsko-posavska zupanija' }, { value: 'HR-19', country_code: 'HR', text: 'Dubrovacko-neretvanska zupanija' }, { value: 'HR-21', country_code: 'HR', text: 'Grad Zagreb' }, { value: 'HR-18', country_code: 'HR', text: 'Istarska zupanija' }, { value: 'HR-04', country_code: 'HR', text: 'Karlovacka zupanija' }, { value: 'HR-06', country_code: 'HR', text: 'Koprivnicko-krizevacka zupanija' }, { value: 'HR-02', country_code: 'HR', text: 'Krapinsko-zagorska zupanija' }, { value: 'HR-09', country_code: 'HR', text: 'Licko-senjska zupanija' }, { value: 'HR-20', country_code: 'HR', text: 'Medimurska zupanija' }, { value: 'HR-14', country_code: 'HR', text: 'Osjecko-baranjska zupanija' }, { value: 'HR-11', country_code: 'HR', text: 'Pozesko-slavonska zupanija' }, { value: 'HR-08', country_code: 'HR', text: 'Primorsko-goranska zupanija' }, { value: 'HR-15', country_code: 'HR', text: 'Sibensko-kninska zupanija' }, { value: 'HR-03', country_code: 'HR', text: 'Sisacko-moslavacka zupanija' }, { value: 'HR-17', country_code: 'HR', text: 'Splitsko-dalmatinska zupanija' }, { value: 'HR-05', country_code: 'HR', text: 'Varazdinska zupanija' }, { value: 'HR-10', country_code: 'HR', text: 'Viroviticko-podravska zupanija' }, { value: 'HR-16', country_code: 'HR', text: 'Vukovarsko-srijemska zupanija' }, { value: 'HR-13', country_code: 'HR', text: 'Zadarska zupanija' }, { value: 'HR-01', country_code: 'HR', text: 'Zagrebacka zupanija' }, { value: 'HT-AR', country_code: 'HT', text: 'Artibonite' }, { value: 'HT-CE', country_code: 'HT', text: 'Centre' }, { value: 'HT-GA', country_code: 'HT', text: "Grande'Anse" }, { value: 'HT-NI', country_code: 'HT', text: 'Nippes' }, { value: 'HT-ND', country_code: 'HT', text: 'Nord' }, { value: 'HT-NE', country_code: 'HT', text: 'Nord-Est' }, { value: 'HT-NO', country_code: 'HT', text: 'Nord-Ouest' }, { value: 'HT-OU', country_code: 'HT', text: 'Ouest' }, { value: 'HT-SD', country_code: 'HT', text: 'Sud' }, { value: 'HT-SE', country_code: 'HT', text: 'Sud-Est' }, { value: 'HU-BK', country_code: 'HU', text: 'Bacs-Kiskun' }, { value: 'HU-BA', country_code: 'HU', text: 'Baranya' }, { value: 'HU-BE', country_code: 'HU', text: 'Bekes' }, { value: 'HU-BZ', country_code: 'HU', text: 'Borsod-Abauj-Zemplen' }, { value: 'HU-BU', country_code: 'HU', text: 'Budapest' }, { value: 'HU-CS', country_code: 'HU', text: 'Csongrad' }, { value: 'HU-FE', country_code: 'HU', text: 'Fejer' }, { value: 'HU-GS', country_code: 'HU', text: 'Gyor-Moson-Sopron' }, { value: 'HU-HB', country_code: 'HU', text: 'Hajdu-Bihar' }, { value: 'HU-HE', country_code: 'HU', text: 'Heves' }, { value: 'HU-JN', country_code: 'HU', text: 'Jasz-Nagykun-Szolnok' }, { value: 'HU-KE', country_code: 'HU', text: 'Komarom-Esztergom' }, { value: 'HU-NO', country_code: 'HU', text: 'Nograd' }, { value: 'HU-PE', country_code: 'HU', text: 'Pest' }, { value: 'HU-SO', country_code: 'HU', text: 'Somogy' }, { value: 'HU-SZ', country_code: 'HU', text: 'Szabolcs-Szatmar-Bereg' }, { value: 'HU-TO', country_code: 'HU', text: 'Tolna' }, { value: 'HU-VA', country_code: 'HU', text: 'Vas' }, { value: 'HU-VM', country_code: 'HU', text: 'Veszprem' }, { value: 'HU-ZA', country_code: 'HU', text: 'Zala' }, { value: 'ID-AC', country_code: 'ID', text: 'Aceh' }, { value: 'ID-BA', country_code: 'ID', text: 'Bali' }, { value: 'ID-BT', country_code: 'ID', text: 'Banten' }, { value: 'ID-BE', country_code: 'ID', text: 'Bengkulu' }, { value: 'ID-GO', country_code: 'ID', text: 'Gorontalo' }, { value: 'ID-JK', country_code: 'ID', text: 'Jakarta Raya' }, { value: 'ID-JA', country_code: 'ID', text: 'Jambi' }, { value: 'ID-JB', country_code: 'ID', text: 'Jawa Barat' }, { value: 'ID-JT', country_code: 'ID', text: 'Jawa Tengah' }, { value: 'ID-JI', country_code: 'ID', text: 'Jawa Timur' }, { value: 'ID-KB', country_code: 'ID', text: 'Kalimantan Barat' }, { value: 'ID-KS', country_code: 'ID', text: 'Kalimantan Selatan' }, { value: 'ID-KT', country_code: 'ID', text: 'Kalimantan Tengah' }, { value: 'ID-KI', country_code: 'ID', text: 'Kalimantan Timur' }, { value: 'ID-BB', country_code: 'ID', text: 'Kepulauan Bangka Belitung' }, { value: 'ID-KR', country_code: 'ID', text: 'Kepulauan Riau' }, { value: 'ID-LA', country_code: 'ID', text: 'Lampung' }, { value: 'ID-ML', country_code: 'ID', text: 'Maluku' }, { value: 'ID-MU', country_code: 'ID', text: 'Maluku Utara' }, { value: 'ID-NB', country_code: 'ID', text: 'Nusa Tenggara Barat' }, { value: 'ID-NT', country_code: 'ID', text: 'Nusa Tenggara Timur' }, { value: 'ID-PP', country_code: 'ID', text: 'Papua' }, { value: 'ID-PB', country_code: 'ID', text: 'Papua Barat' }, { value: 'ID-RI', country_code: 'ID', text: 'Riau' }, { value: 'ID-SR', country_code: 'ID', text: 'Sulawesi Barat' }, { value: 'ID-SN', country_code: 'ID', text: 'Sulawesi Selatan' }, { value: 'ID-ST', country_code: 'ID', text: 'Sulawesi Tengah' }, { value: 'ID-SG', country_code: 'ID', text: 'Sulawesi Tenggara' }, { value: 'ID-SA', country_code: 'ID', text: 'Sulawesi Utara' }, { value: 'ID-SB', country_code: 'ID', text: 'Sumatera Barat' }, { value: 'ID-SS', country_code: 'ID', text: 'Sumatera Selatan' }, { value: 'ID-SU', country_code: 'ID', text: 'Sumatera Utara' }, { value: 'ID-YO', country_code: 'ID', text: 'Yogyakarta' }, { value: 'IE-CW', country_code: 'IE', text: 'Carlow' }, { value: 'IE-CN', country_code: 'IE', text: 'Cavan' }, { value: 'IE-CE', country_code: 'IE', text: 'Clare' }, { value: 'IE-CO', country_code: 'IE', text: 'Cork' }, { value: 'IE-DL', country_code: 'IE', text: 'Donegal' }, { value: 'IE-D', country_code: 'IE', text: 'Dublin' }, { value: 'IE-G', country_code: 'IE', text: 'Galway' }, { value: 'IE-KY', country_code: 'IE', text: 'Kerry' }, { value: 'IE-KE', country_code: 'IE', text: 'Kildare' }, { value: 'IE-KK', country_code: 'IE', text: 'Kilkenny' }, { value: 'IE-LS', country_code: 'IE', text: 'Laois' }, { value: 'IE-LM', country_code: 'IE', text: 'Leitrim' }, { value: 'IE-LK', country_code: 'IE', text: 'Limerick' }, { value: 'IE-LD', country_code: 'IE', text: 'Longford' }, { value: 'IE-LH', country_code: 'IE', text: 'Louth' }, { value: 'IE-MO', country_code: 'IE', text: 'Mayo' }, { value: 'IE-MH', country_code: 'IE', text: 'Meath' }, { value: 'IE-MN', country_code: 'IE', text: 'Monaghan' }, { value: 'IE-OY', country_code: 'IE', text: 'Offaly' }, { value: 'IE-RN', country_code: 'IE', text: 'Roscommon' }, { value: 'IE-SO', country_code: 'IE', text: 'Sligo' }, { value: 'IE-TA', country_code: 'IE', text: 'Tipperary' }, { value: 'IE-WD', country_code: 'IE', text: 'Waterford' }, { value: 'IE-WH', country_code: 'IE', text: 'Westmeath' }, { value: 'IE-WX', country_code: 'IE', text: 'Wexford' }, { value: 'IE-WW', country_code: 'IE', text: 'Wicklow' }, { value: 'IL-D', country_code: 'IL', text: 'HaDarom' }, { value: 'IL-M', country_code: 'IL', text: 'HaMerkaz' }, { value: 'IL-Z', country_code: 'IL', text: 'HaTsafon' }, { value: 'IL-HA', country_code: 'IL', text: 'Hefa' }, { value: 'IL-TA', country_code: 'IL', text: 'Tel Aviv' }, { value: 'IL-JM', country_code: 'IL', text: 'Yerushalayim' }, { value: 'IN-AN', country_code: 'IN', text: 'Andaman and Nicobar Islands' }, { value: 'IN-AP', country_code: 'IN', text: 'Andhra Pradesh' }, { value: 'IN-AR', country_code: 'IN', text: 'Arunachal Pradesh' }, { value: 'IN-AS', country_code: 'IN', text: 'Assam' }, { value: 'IN-BR', country_code: 'IN', text: 'Bihar' }, { value: 'IN-CH', country_code: 'IN', text: 'Chandigarh' }, { value: 'IN-CT', country_code: 'IN', text: 'Chhattisgarh' }, { value: 'IN-DN', country_code: 'IN', text: 'Dadra and Nagar Haveli' }, { value: 'IN-DD', country_code: 'IN', text: 'Daman and Diu' }, { value: 'IN-DL', country_code: 'IN', text: 'Delhi' }, { value: 'IN-GA', country_code: 'IN', text: 'Goa' }, { value: 'IN-GJ', country_code: 'IN', text: 'Gujarat' }, { value: 'IN-HR', country_code: 'IN', text: 'Haryana' }, { value: 'IN-HP', country_code: 'IN', text: 'Himachal Pradesh' }, { value: 'IN-JK', country_code: 'IN', text: 'Jammu and Kashmir' }, { value: 'IN-JH', country_code: 'IN', text: 'Jharkhand' }, { value: 'IN-KA', country_code: 'IN', text: 'Karnataka' }, { value: 'IN-KL', country_code: 'IN', text: 'Kerala' }, { value: 'IN-LD', country_code: 'IN', text: 'Lakshadweep' }, { value: 'IN-MP', country_code: 'IN', text: 'Madhya Pradesh' }, { value: 'IN-MH', country_code: 'IN', text: 'Maharashtra' }, { value: 'IN-MN', country_code: 'IN', text: 'Manipur' }, { value: 'IN-ML', country_code: 'IN', text: 'Meghalaya' }, { value: 'IN-MZ', country_code: 'IN', text: 'Mizoram' }, { value: 'IN-NL', country_code: 'IN', text: 'Nagaland' }, { value: 'IN-OR', country_code: 'IN', text: 'Odisha' }, { value: 'IN-PY', country_code: 'IN', text: 'Puducherry' }, { value: 'IN-PB', country_code: 'IN', text: 'Punjab' }, { value: 'IN-RJ', country_code: 'IN', text: 'Rajasthan' }, { value: 'IN-SK', country_code: 'IN', text: 'Sikkim' }, { value: 'IN-TN', country_code: 'IN', text: 'Tamil Nadu' }, { value: 'IN-TG', country_code: 'IN', text: 'Telangana' }, { value: 'IN-TR', country_code: 'IN', text: 'Tripura' }, { value: 'IN-UP', country_code: 'IN', text: 'Uttar Pradesh' }, { value: 'IN-UT', country_code: 'IN', text: 'Uttarakhand' }, { value: 'IN-WB', country_code: 'IN', text: 'West Bengal' }, { value: 'IQ-AN', country_code: 'IQ', text: 'Al Anbar' }, { value: 'IQ-BA', country_code: 'IQ', text: 'Al Basrah' }, { value: 'IQ-MU', country_code: 'IQ', text: 'Al Muthanna' }, { value: 'IQ-QA', country_code: 'IQ', text: 'Al Qadisiyah' }, { value: 'IQ-NA', country_code: 'IQ', text: 'An Najaf' }, { value: 'IQ-AR', country_code: 'IQ', text: 'Arbil' }, { value: 'IQ-SU', country_code: 'IQ', text: 'As Sulaymaniyah' }, { value: 'IQ-BB', country_code: 'IQ', text: 'Babil' }, { value: 'IQ-BG', country_code: 'IQ', text: 'Baghdad' }, { value: 'IQ-DA', country_code: 'IQ', text: 'Dahuk' }, { value: 'IQ-DQ', country_code: 'IQ', text: 'Dhi Qar' }, { value: 'IQ-DI', country_code: 'IQ', text: 'Diyala' }, { value: 'IQ-KA', country_code: 'IQ', text: "Karbala'" }, { value: 'IQ-KI', country_code: 'IQ', text: 'Kirkuk' }, { value: 'IQ-MA', country_code: 'IQ', text: 'Maysan' }, { value: 'IQ-NI', country_code: 'IQ', text: 'Ninawa' }, { value: 'IQ-SD', country_code: 'IQ', text: 'Salah ad Din' }, { value: 'IQ-WA', country_code: 'IQ', text: 'Wasit' }, { value: 'IR-32', country_code: 'IR', text: 'Alborz' }, { value: 'IR-03', country_code: 'IR', text: 'Ardabil' }, { value: 'IR-02', country_code: 'IR', text: 'Azarbayjan-e Gharbi' }, { value: 'IR-01', country_code: 'IR', text: 'Azarbayjan-e Sharqi' }, { value: 'IR-06', country_code: 'IR', text: 'Bushehr' }, { value: 'IR-08', country_code: 'IR', text: 'Chahar Mahal va Bakhtiari' }, { value: 'IR-04', country_code: 'IR', text: 'Esfahan' }, { value: 'IR-14', country_code: 'IR', text: 'Fars' }, { value: 'IR-19', country_code: 'IR', text: 'Gilan' }, { value: 'IR-27', country_code: 'IR', text: 'Golestan' }, { value: 'IR-24', country_code: 'IR', text: 'Hamadan' }, { value: 'IR-23', country_code: 'IR', text: 'Hormozgan' }, { value: 'IR-05', country_code: 'IR', text: 'Ilam' }, { value: 'IR-15', country_code: 'IR', text: 'Kerman' }, { value: 'IR-17', country_code: 'IR', text: 'Kermanshah' }, { value: 'IR-29', country_code: 'IR', text: 'Khorasan-e Jonubi' }, { value: 'IR-30', country_code: 'IR', text: 'Khorasan-e Razavi' }, { value: 'IR-31', country_code: 'IR', text: 'Khorasan-e Shomali' }, { value: 'IR-10', country_code: 'IR', text: 'Khuzestan' }, { value: 'IR-18', country_code: 'IR', text: 'Kohgiluyeh va Bowyer Ahmad' }, { value: 'IR-16', country_code: 'IR', text: 'Kordestan' }, { value: 'IR-20', country_code: 'IR', text: 'Lorestan' }, { value: 'IR-22', country_code: 'IR', text: 'Markazi' }, { value: 'IR-21', country_code: 'IR', text: 'Mazandaran' }, { value: 'IR-28', country_code: 'IR', text: 'Qazvin' }, { value: 'IR-26', country_code: 'IR', text: 'Qom' }, { value: 'IR-12', country_code: 'IR', text: 'Semnan' }, { value: 'IR-13', country_code: 'IR', text: 'Sistan va Baluchestan' }, { value: 'IR-07', country_code: 'IR', text: 'Tehran' }, { value: 'IR-25', country_code: 'IR', text: 'Yazd' }, { value: 'IR-11', country_code: 'IR', text: 'Zanjan' }, { value: 'IS-7', country_code: 'IS', text: 'Austurland' }, { value: 'IS-1', country_code: 'IS', text: 'Hofudborgarsvaedi utan Reykjavikur' }, { value: 'IS-6', country_code: 'IS', text: 'Nordurland eystra' }, { value: 'IS-5', country_code: 'IS', text: 'Nordurland vestra' }, { value: 'IS-8', country_code: 'IS', text: 'Sudurland' }, { value: 'IS-2', country_code: 'IS', text: 'Sudurnes' }, { value: 'IS-4', country_code: 'IS', text: 'Vestfirdir' }, { value: 'IS-3', country_code: 'IS', text: 'Vesturland' }, { value: 'IT-65', country_code: 'IT', text: 'Abruzzo' }, { value: 'IT-77', country_code: 'IT', text: 'Basilicata' }, { value: 'IT-78', country_code: 'IT', text: 'Calabria' }, { value: 'IT-72', country_code: 'IT', text: 'Campania' }, { value: 'IT-45', country_code: 'IT', text: 'Emilia-Romagna' }, { value: 'IT-36', country_code: 'IT', text: 'Friuli-Venezia Giulia' }, { value: 'IT-62', country_code: 'IT', text: 'Lazio' }, { value: 'IT-42', country_code: 'IT', text: 'Liguria' }, { value: 'IT-25', country_code: 'IT', text: 'Lombardia' }, { value: 'IT-57', country_code: 'IT', text: 'Marche' }, { value: 'IT-67', country_code: 'IT', text: 'Molise' }, { value: 'IT-21', country_code: 'IT', text: 'Piemonte' }, { value: 'IT-75', country_code: 'IT', text: 'Puglia' }, { value: 'IT-88', country_code: 'IT', text: 'Sardegna' }, { value: 'IT-82', country_code: 'IT', text: 'Sicilia' }, { value: 'IT-52', country_code: 'IT', text: 'Toscana' }, { value: 'IT-32', country_code: 'IT', text: 'Trentino-Alto Adige' }, { value: 'IT-55', country_code: 'IT', text: 'Umbria' }, { value: 'IT-23', country_code: 'IT', text: "Valle d'Aosta" }, { value: 'IT-34', country_code: 'IT', text: 'Veneto' }, { value: 'JM-13', country_code: 'JM', text: 'Clarendon' }, { value: 'JM-09', country_code: 'JM', text: 'Hanover' }, { value: 'JM-01', country_code: 'JM', text: 'Kingston' }, { value: 'JM-12', country_code: 'JM', text: 'Manchester' }, { value: 'JM-04', country_code: 'JM', text: 'Portland' }, { value: 'JM-02', country_code: 'JM', text: 'Saint Andrew' }, { value: 'JM-06', country_code: 'JM', text: 'Saint Ann' }, { value: 'JM-14', country_code: 'JM', text: 'Saint Catherine' }, { value: 'JM-11', country_code: 'JM', text: 'Saint Elizabeth' }, { value: 'JM-08', country_code: 'JM', text: 'Saint James' }, { value: 'JM-05', country_code: 'JM', text: 'Saint Mary' }, { value: 'JM-03', country_code: 'JM', text: 'Saint Thomas' }, { value: 'JM-07', country_code: 'JM', text: 'Trelawny' }, { value: 'JM-10', country_code: 'JM', text: 'Westmoreland' }, { value: 'JO-AQ', country_code: 'JO', text: "Al 'Aqabah" }, { value: 'JO-AM', country_code: 'JO', text: "Al 'Asimah" }, { value: 'JO-BA', country_code: 'JO', text: "Al Balqa'" }, { value: 'JO-KA', country_code: 'JO', text: 'Al Karak' }, { value: 'JO-MA', country_code: 'JO', text: 'Al Mafraq' }, { value: 'JO-AT', country_code: 'JO', text: 'At Tafilah' }, { value: 'JO-AZ', country_code: 'JO', text: "Az Zarqa'" }, { value: 'JO-IR', country_code: 'JO', text: 'Irbid' }, { value: 'JO-MN', country_code: 'JO', text: "Ma'an" }, { value: 'JO-MD', country_code: 'JO', text: 'Madaba' }, { value: 'JP-23', country_code: 'JP', text: 'Aichi' }, { value: 'JP-05', country_code: 'JP', text: 'Akita' }, { value: 'JP-02', country_code: 'JP', text: 'Aomori' }, { value: 'JP-12', country_code: 'JP', text: 'Chiba' }, { value: 'JP-38', country_code: 'JP', text: 'Ehime' }, { value: 'JP-18', country_code: 'JP', text: 'Fukui' }, { value: 'JP-40', country_code: 'JP', text: 'Fukuoka' }, { value: 'JP-07', country_code: 'JP', text: 'Fukushima' }, { value: 'JP-21', country_code: 'JP', text: 'Gifu' }, { value: 'JP-10', country_code: 'JP', text: 'Gunma' }, { value: 'JP-34', country_code: 'JP', text: 'Hiroshima' }, { value: 'JP-01', country_code: 'JP', text: 'Hokkaido' }, { value: 'JP-28', country_code: 'JP', text: 'Hyogo' }, { value: 'JP-08', country_code: 'JP', text: 'Ibaraki' }, { value: 'JP-17', country_code: 'JP', text: 'Ishikawa' }, { value: 'JP-03', country_code: 'JP', text: 'Iwate' }, { value: 'JP-37', country_code: 'JP', text: 'Kagawa' }, { value: 'JP-46', country_code: 'JP', text: 'Kagoshima' }, { value: 'JP-14', country_code: 'JP', text: 'Kanagawa' }, { value: 'JP-39', country_code: 'JP', text: 'Kochi' }, { value: 'JP-43', country_code: 'JP', text: 'Kumamoto' }, { value: 'JP-26', country_code: 'JP', text: 'Kyoto' }, { value: 'JP-24', country_code: 'JP', text: 'Mie' }, { value: 'JP-04', country_code: 'JP', text: 'Miyagi' }, { value: 'JP-45', country_code: 'JP', text: 'Miyazaki' }, { value: 'JP-20', country_code: 'JP', text: 'Nagano' }, { value: 'JP-42', country_code: 'JP', text: 'Nagasaki' }, { value: 'JP-29', country_code: 'JP', text: 'Nara' }, { value: 'JP-15', country_code: 'JP', text: 'Niigata' }, { value: 'JP-44', country_code: 'JP', text: 'Oita' }, { value: 'JP-33', country_code: 'JP', text: 'Okayama' }, { value: 'JP-47', country_code: 'JP', text: 'Okinawa' }, { value: 'JP-27', country_code: 'JP', text: 'Osaka' }, { value: 'JP-41', country_code: 'JP', text: 'Saga' }, { value: 'JP-11', country_code: 'JP', text: 'Saitama' }, { value: 'JP-25', country_code: 'JP', text: 'Shiga' }, { value: 'JP-32', country_code: 'JP', text: 'Shimane' }, { value: 'JP-22', country_code: 'JP', text: 'Shizuoka' }, { value: 'JP-09', country_code: 'JP', text: 'Tochigi' }, { value: 'JP-36', country_code: 'JP', text: 'Tokushima' }, { value: 'JP-13', country_code: 'JP', text: 'Tokyo' }, { value: 'JP-31', country_code: 'JP', text: 'Tottori' }, { value: 'JP-16', country_code: 'JP', text: 'Toyama' }, { value: 'JP-30', country_code: 'JP', text: 'Wakayama' }, { value: 'JP-06', country_code: 'JP', text: 'Yamagata' }, { value: 'JP-35', country_code: 'JP', text: 'Yamaguchi' }, { value: 'JP-19', country_code: 'JP', text: 'Yamanashi' }, { value: 'KE-01', country_code: 'KE', text: 'Baringo' }, { value: 'KE-02', country_code: 'KE', text: 'Bomet' }, { value: 'KE-03', country_code: 'KE', text: 'Bungoma' }, { value: 'KE-04', country_code: 'KE', text: 'Busia' }, { value: 'KE-06', country_code: 'KE', text: 'Embu' }, { value: 'KE-07', country_code: 'KE', text: 'Garissa' }, { value: 'KE-08', country_code: 'KE', text: 'Homa Bay' }, { value: 'KE-09', country_code: 'KE', text: 'Isiolo' }, { value: 'KE-10', country_code: 'KE', text: 'Kajiado' }, { value: 'KE-11', country_code: 'KE', text: 'Kakamega' }, { value: 'KE-12', country_code: 'KE', text: 'Kericho' }, { value: 'KE-13', country_code: 'KE', text: 'Kiambu' }, { value: 'KE-14', country_code: 'KE', text: 'Kilifi' }, { value: 'KE-15', country_code: 'KE', text: 'Kirinyaga' }, { value: 'KE-16', country_code: 'KE', text: 'Kisii' }, { value: 'KE-17', country_code: 'KE', text: 'Kisumu' }, { value: 'KE-18', country_code: 'KE', text: 'Kitui' }, { value: 'KE-19', country_code: 'KE', text: 'Kwale' }, { value: 'KE-20', country_code: 'KE', text: 'Laikipia' }, { value: 'KE-21', country_code: 'KE', text: 'Lamu' }, { value: 'KE-22', country_code: 'KE', text: 'Machakos' }, { value: 'KE-23', country_code: 'KE', text: 'Makueni' }, { value: 'KE-24', country_code: 'KE', text: 'Mandera' }, { value: 'KE-25', country_code: 'KE', text: 'Marsabit' }, { value: 'KE-26', country_code: 'KE', text: 'Meru' }, { value: 'KE-27', country_code: 'KE', text: 'Migori' }, { value: 'KE-28', country_code: 'KE', text: 'Mombasa' }, { value: 'KE-29', country_code: 'KE', text: "Murang'a" }, { value: 'KE-30', country_code: 'KE', text: 'Nairobi City' }, { value: 'KE-31', country_code: 'KE', text: 'Nakuru' }, { value: 'KE-32', country_code: 'KE', text: 'Nandi' }, { value: 'KE-33', country_code: 'KE', text: 'Narok' }, { value: 'KE-34', country_code: 'KE', text: 'Nyamira' }, { value: 'KE-36', country_code: 'KE', text: 'Nyeri' }, { value: 'KE-37', country_code: 'KE', text: 'Samburu' }, { value: 'KE-38', country_code: 'KE', text: 'Siaya' }, { value: 'KE-39', country_code: 'KE', text: 'Taita/Taveta' }, { value: 'KE-40', country_code: 'KE', text: 'Tana River' }, { value: 'KE-41', country_code: 'KE', text: 'Tharaka-Nithi' }, { value: 'KE-42', country_code: 'KE', text: 'Trans Nzoia' }, { value: 'KE-43', country_code: 'KE', text: 'Turkana' }, { value: 'KE-44', country_code: 'KE', text: 'Uasin Gishu' }, { value: 'KE-45', country_code: 'KE', text: 'Vihiga' }, { value: 'KE-46', country_code: 'KE', text: 'Wajir' }, { value: 'KE-47', country_code: 'KE', text: 'West Pokot' }, { value: 'KG-B', country_code: 'KG', text: 'Batken' }, { value: 'KG-GB', country_code: 'KG', text: 'Bishkek' }, { value: 'KG-C', country_code: 'KG', text: 'Chuy' }, { value: 'KG-J', country_code: 'KG', text: 'Jalal-Abad' }, { value: 'KG-N', country_code: 'KG', text: 'Naryn' }, { value: 'KG-GO', country_code: 'KG', text: 'Osh' }, { value: 'KG-T', country_code: 'KG', text: 'Talas' }, { value: 'KG-Y', country_code: 'KG', text: 'Ysyk-Kol' }, { value: 'KH-2', country_code: 'KH', text: 'Baat Dambang' }, { value: 'KH-1', country_code: 'KH', text: 'Banteay Mean Chey' }, { value: 'KH-3', country_code: 'KH', text: 'Kampong Chaam' }, { value: 'KH-4', country_code: 'KH', text: 'Kampong Chhnang' }, { value: 'KH-5', country_code: 'KH', text: 'Kampong Spueu' }, { value: 'KH-6', country_code: 'KH', text: 'Kampong Thum' }, { value: 'KH-7', country_code: 'KH', text: 'Kampot' }, { value: 'KH-8', country_code: 'KH', text: 'Kandaal' }, { value: 'KH-9', country_code: 'KH', text: 'Kaoh Kong' }, { value: 'KH-10', country_code: 'KH', text: 'Kracheh' }, { value: 'KH-23', country_code: 'KH', text: 'Krong Kaeb' }, { value: 'KH-24', country_code: 'KH', text: 'Krong Pailin' }, { value: 'KH-18', country_code: 'KH', text: 'Krong Preah Sihanouk' }, { value: 'KH-11', country_code: 'KH', text: 'Mondol Kiri' }, { value: 'KH-22', country_code: 'KH', text: 'Otdar Mean Chey' }, { value: 'KH-12', country_code: 'KH', text: 'Phnom Penh' }, { value: 'KH-15', country_code: 'KH', text: 'Pousaat' }, { value: 'KH-13', country_code: 'KH', text: 'Preah Vihear' }, { value: 'KH-14', country_code: 'KH', text: 'Prey Veaeng' }, { value: 'KH-16', country_code: 'KH', text: 'Rotanak Kiri' }, { value: 'KH-17', country_code: 'KH', text: 'Siem Reab' }, { value: 'KH-19', country_code: 'KH', text: 'Stueng Traeng' }, { value: 'KH-20', country_code: 'KH', text: 'Svaay Rieng' }, { value: 'KH-21', country_code: 'KH', text: 'Taakaev' }, { value: 'KI-G', country_code: 'KI', text: 'Gilbert Islands' }, { value: 'KI-L', country_code: 'KI', text: 'Line Islands' }, { value: 'KM-A', country_code: 'KM', text: 'Anjouan' }, { value: 'KM-G', country_code: 'KM', text: 'Grande Comore' }, { value: 'KM-M', country_code: 'KM', text: 'Moheli' }, { value: 'KN-03', country_code: 'KN', text: 'Saint George Basseterre' }, { value: 'KN-10', country_code: 'KN', text: 'Saint Paul Charlestown' }, { value: 'KP-04', country_code: 'KP', text: 'Chagang-do' }, { value: 'KP-09', country_code: 'KP', text: 'Hamgyong-bukto' }, { value: 'KP-08', country_code: 'KP', text: 'Hamgyong-namdo' }, { value: 'KP-06', country_code: 'KP', text: 'Hwanghae-bukto' }, { value: 'KP-05', country_code: 'KP', text: 'Hwanghae-namdo' }, { value: 'KP-07', country_code: 'KP', text: 'Kangwon-do' }, { value: 'KP-13', country_code: 'KP', text: 'Nason' }, { value: 'KP-03', country_code: 'KP', text: "P'yongan-bukto" }, { value: 'KP-02', country_code: 'KP', text: "P'yongan-namdo" }, { value: 'KP-01', country_code: 'KP', text: "P'yongyang" }, { value: 'KP-10', country_code: 'KP', text: 'Yanggang-do' }, { value: 'KR-26', country_code: 'KR', text: 'Busan-gwangyeoksi' }, { value: 'KR-43', country_code: 'KR', text: 'Chungcheongbuk-do' }, { value: 'KR-44', country_code: 'KR', text: 'Chungcheongnam-do' }, { value: 'KR-27', country_code: 'KR', text: 'Daegu-gwangyeoksi' }, { value: 'KR-30', country_code: 'KR', text: 'Daejeon-gwangyeoksi' }, { value: 'KR-42', country_code: 'KR', text: 'Gangwon-do' }, { value: 'KR-29', country_code: 'KR', text: 'Gwangju-gwangyeoksi' }, { value: 'KR-41', country_code: 'KR', text: 'Gyeonggi-do' }, { value: 'KR-47', country_code: 'KR', text: 'Gyeongsangbuk-do' }, { value: 'KR-48', country_code: 'KR', text: 'Gyeongsangnam-do' }, { value: 'KR-28', country_code: 'KR', text: 'Incheon-gwangyeoksi' }, { value: 'KR-49', country_code: 'KR', text: 'Jeju-teukbyeoljachido' }, { value: 'KR-45', country_code: 'KR', text: 'Jeollabuk-do' }, { value: 'KR-46', country_code: 'KR', text: 'Jeollanam-do' }, { value: 'KR-11', country_code: 'KR', text: 'Seoul-teukbyeolsi' }, { value: 'KR-31', country_code: 'KR', text: 'Ulsan-gwangyeoksi' }, { value: 'KW-KU', country_code: 'KW', text: "Al 'Asimah" }, { value: 'KW-AH', country_code: 'KW', text: 'Al Ahmadi' }, { value: 'KW-FA', country_code: 'KW', text: 'Al Farwaniyah' }, { value: 'KW-JA', country_code: 'KW', text: 'Al Jahra' }, { value: 'KW-HA', country_code: 'KW', text: 'Hawalli' }, { value: 'KW-MU', country_code: 'KW', text: 'Mubarak al Kabir' }, { value: 'KZ-ALA', country_code: 'KZ', text: 'Almaty' }, { value: 'KZ-ALM', country_code: 'KZ', text: 'Almaty oblysy' }, { value: 'KZ-AKM', country_code: 'KZ', text: 'Aqmola oblysy' }, { value: 'KZ-AKT', country_code: 'KZ', text: 'Aqtobe oblysy' }, { value: 'KZ-AST', country_code: 'KZ', text: 'Astana' }, { value: 'KZ-ATY', country_code: 'KZ', text: 'Atyrau oblysy' }, { value: 'KZ-ZAP', country_code: 'KZ', text: 'Batys Qazaqstan oblysy' }, { value: 'KZ-BAY', country_code: 'KZ', text: 'Bayqongyr' }, { value: 'KZ-MAN', country_code: 'KZ', text: 'Mangghystau oblysy' }, { value: 'KZ-YUZ', country_code: 'KZ', text: 'Ongtustik Qazaqstan oblysy' }, { value: 'KZ-PAV', country_code: 'KZ', text: 'Pavlodar oblysy' }, { value: 'KZ-KAR', country_code: 'KZ', text: 'Qaraghandy oblysy' }, { value: 'KZ-KUS', country_code: 'KZ', text: 'Qostanay oblysy' }, { value: 'KZ-KZY', country_code: 'KZ', text: 'Qyzylorda oblysy' }, { value: 'KZ-VOS', country_code: 'KZ', text: 'Shyghys Qazaqstan oblysy' }, { value: 'KZ-SEV', country_code: 'KZ', text: 'Soltustik Qazaqstan oblysy' }, { value: 'KZ-ZHA', country_code: 'KZ', text: 'Zhambyl oblysy' }, { value: 'LA-AT', country_code: 'LA', text: 'Attapu' }, { value: 'LA-BK', country_code: 'LA', text: 'Bokeo' }, { value: 'LA-BL', country_code: 'LA', text: 'Bolikhamxai' }, { value: 'LA-CH', country_code: 'LA', text: 'Champasak' }, { value: 'LA-HO', country_code: 'LA', text: 'Houaphan' }, { value: 'LA-KH', country_code: 'LA', text: 'Khammouan' }, { value: 'LA-LM', country_code: 'LA', text: 'Louang Namtha' }, { value: 'LA-LP', country_code: 'LA', text: 'Louangphabang' }, { value: 'LA-OU', country_code: 'LA', text: 'Oudomxai' }, { value: 'LA-PH', country_code: 'LA', text: 'Phongsali' }, { value: 'LA-SL', country_code: 'LA', text: 'Salavan' }, { value: 'LA-SV', country_code: 'LA', text: 'Savannakhet' }, { value: 'LA-VI', country_code: 'LA', text: 'Viangchan' }, { value: 'LA-XA', country_code: 'LA', text: 'Xaignabouli' }, { value: 'LA-XE', country_code: 'LA', text: 'Xekong' }, { value: 'LA-XI', country_code: 'LA', text: 'Xiangkhouang' }, { value: 'LB-AK', country_code: 'LB', text: 'Aakkar' }, { value: 'LB-BH', country_code: 'LB', text: 'Baalbek-Hermel' }, { value: 'LB-BI', country_code: 'LB', text: 'Beqaa' }, { value: 'LB-BA', country_code: 'LB', text: 'Beyrouth' }, { value: 'LB-AS', country_code: 'LB', text: 'Liban-Nord' }, { value: 'LB-JA', country_code: 'LB', text: 'Liban-Sud' }, { value: 'LB-JL', country_code: 'LB', text: 'Mont-Liban' }, { value: 'LB-NA', country_code: 'LB', text: 'Nabatiye' }, { value: 'LC-01', country_code: 'LC', text: 'Anse la Raye' }, { value: 'LC-02', country_code: 'LC', text: 'Castries' }, { value: 'LC-05', country_code: 'LC', text: 'Dennery' }, { value: 'LC-06', country_code: 'LC', text: 'Gros Islet' }, { value: 'LC-07', country_code: 'LC', text: 'Laborie' }, { value: 'LC-08', country_code: 'LC', text: 'Micoud' }, { value: 'LC-10', country_code: 'LC', text: 'Soufriere' }, { value: 'LC-11', country_code: 'LC', text: 'Vieux Fort' }, { value: 'LI-01', country_code: 'LI', text: 'Balzers' }, { value: 'LI-02', country_code: 'LI', text: 'Eschen' }, { value: 'LI-03', country_code: 'LI', text: 'Gamprin' }, { value: 'LI-04', country_code: 'LI', text: 'Mauren' }, { value: 'LI-05', country_code: 'LI', text: 'Planken' }, { value: 'LI-06', country_code: 'LI', text: 'Ruggell' }, { value: 'LI-07', country_code: 'LI', text: 'Schaan' }, { value: 'LI-08', country_code: 'LI', text: 'Schellenberg' }, { value: 'LI-09', country_code: 'LI', text: 'Triesen' }, { value: 'LI-10', country_code: 'LI', text: 'Triesenberg' }, { value: 'LI-11', country_code: 'LI', text: 'Vaduz' }, { value: 'LK-2', country_code: 'LK', text: 'Central Province' }, { value: 'LK-5', country_code: 'LK', text: 'Eastern Province' }, { value: 'LK-7', country_code: 'LK', text: 'North Central Province' }, { value: 'LK-6', country_code: 'LK', text: 'North Western Province' }, { value: 'LK-4', country_code: 'LK', text: 'Northern Province' }, { value: 'LK-9', country_code: 'LK', text: 'Sabaragamuwa Province' }, { value: 'LK-3', country_code: 'LK', text: 'Southern Province' }, { value: 'LK-8', country_code: 'LK', text: 'Uva Province' }, { value: 'LK-1', country_code: 'LK', text: 'Western Province' }, { value: 'LR-BM', country_code: 'LR', text: 'Bomi' }, { value: 'LR-BG', country_code: 'LR', text: 'Bong' }, { value: 'LR-GP', country_code: 'LR', text: 'Gbarpolu' }, { value: 'LR-GB', country_code: 'LR', text: 'Grand Bassa' }, { value: 'LR-CM', country_code: 'LR', text: 'Grand Cape Mount' }, { value: 'LR-GG', country_code: 'LR', text: 'Grand Gedeh' }, { value: 'LR-GK', country_code: 'LR', text: 'Grand Kru' }, { value: 'LR-LO', country_code: 'LR', text: 'Lofa' }, { value: 'LR-MG', country_code: 'LR', text: 'Margibi' }, { value: 'LR-MY', country_code: 'LR', text: 'Maryland' }, { value: 'LR-MO', country_code: 'LR', text: 'Montserrado' }, { value: 'LR-NI', country_code: 'LR', text: 'Nimba' }, { value: 'LR-RI', country_code: 'LR', text: 'River Cess' }, { value: 'LR-RG', country_code: 'LR', text: 'River Gee' }, { value: 'LR-SI', country_code: 'LR', text: 'Sinoe' }, { value: 'LS-D', country_code: 'LS', text: 'Berea' }, { value: 'LS-B', country_code: 'LS', text: 'Butha-Buthe' }, { value: 'LS-C', country_code: 'LS', text: 'Leribe' }, { value: 'LS-E', country_code: 'LS', text: 'Mafeteng' }, { value: 'LS-A', country_code: 'LS', text: 'Maseru' }, { value: 'LS-F', country_code: 'LS', text: "Mohale's Hoek" }, { value: 'LS-J', country_code: 'LS', text: 'Mokhotlong' }, { value: 'LS-H', country_code: 'LS', text: "Qacha's Nek" }, { value: 'LS-G', country_code: 'LS', text: 'Quthing' }, { value: 'LS-K', country_code: 'LS', text: 'Thaba-Tseka' }, { value: 'LT-AL', country_code: 'LT', text: 'Alytaus apskritis' }, { value: 'LT-KU', country_code: 'LT', text: 'Kauno apskritis' }, { value: 'LT-KL', country_code: 'LT', text: 'Klaipedos apskritis' }, { value: 'LT-MR', country_code: 'LT', text: 'Marijampoles apskritis' }, { value: 'LT-PN', country_code: 'LT', text: 'Panevezio apskritis' }, { value: 'LT-SA', country_code: 'LT', text: 'Siauliu apskritis' }, { value: 'LT-TA', country_code: 'LT', text: 'Taurages apskritis' }, { value: 'LT-TE', country_code: 'LT', text: 'Telsiu apskritis' }, { value: 'LT-UT', country_code: 'LT', text: 'Utenos apskritis' }, { value: 'LT-VL', country_code: 'LT', text: 'Vilniaus apskritis' }, { value: 'LU-DI', country_code: 'LU', text: 'Diekirch' }, { value: 'LU-GR', country_code: 'LU', text: 'Grevenmacher' }, { value: 'LU-LU', country_code: 'LU', text: 'Luxembourg' }, { value: 'LV-011', country_code: 'LV', text: 'Adazu novads' }, { value: 'LV-001', country_code: 'LV', text: 'Aglonas novads' }, { value: 'LV-002', country_code: 'LV', text: 'Aizkraukles novads' }, { value: 'LV-003', country_code: 'LV', text: 'Aizputes novads' }, { value: 'LV-005', country_code: 'LV', text: 'Alojas novads' }, { value: 'LV-007', country_code: 'LV', text: 'Aluksnes novads' }, { value: 'LV-012', country_code: 'LV', text: 'Babites novads' }, { value: 'LV-014', country_code: 'LV', text: 'Baltinavas novads' }, { value: 'LV-015', country_code: 'LV', text: 'Balvu novads' }, { value: 'LV-016', country_code: 'LV', text: 'Bauskas novads' }, { value: 'LV-017', country_code: 'LV', text: 'Beverinas novads' }, { value: 'LV-018', country_code: 'LV', text: 'Brocenu novads' }, { value: 'LV-020', country_code: 'LV', text: 'Carnikavas novads' }, { value: 'LV-022', country_code: 'LV', text: 'Cesu novads' }, { value: 'LV-021', country_code: 'LV', text: 'Cesvaines novads' }, { value: 'LV-023', country_code: 'LV', text: 'Ciblas novads' }, { value: 'LV-025', country_code: 'LV', text: 'Daugavpils novads' }, { value: 'LV-026', country_code: 'LV', text: 'Dobeles novads' }, { value: 'LV-027', country_code: 'LV', text: 'Dundagas novads' }, { value: 'LV-033', country_code: 'LV', text: 'Gulbenes novads' }, { value: 'LV-034', country_code: 'LV', text: 'Iecavas novads' }, { value: 'LV-037', country_code: 'LV', text: 'Incukalna novads' }, { value: 'LV-038', country_code: 'LV', text: 'Jaunjelgavas novads' }, { value: 'LV-039', country_code: 'LV', text: 'Jaunpiebalgas novads' }, { value: 'LV-040', country_code: 'LV', text: 'Jaunpils novads' }, { value: 'LV-042', country_code: 'LV', text: 'Jekabpils novads' }, { value: 'LV-JEL', country_code: 'LV', text: 'Jelgava' }, { value: 'LV-041', country_code: 'LV', text: 'Jelgavas novads' }, { value: 'LV-JUR', country_code: 'LV', text: 'Jurmala' }, { value: 'LV-052', country_code: 'LV', text: 'Kekavas novads' }, { value: 'LV-046', country_code: 'LV', text: 'Kokneses novads' }, { value: 'LV-047', country_code: 'LV', text: 'Kraslavas novads' }, { value: 'LV-050', country_code: 'LV', text: 'Kuldigas novads' }, { value: 'LV-LPX', country_code: 'LV', text: 'Liepaja' }, { value: 'LV-054', country_code: 'LV', text: 'Limbazu novads' }, { value: 'LV-057', country_code: 'LV', text: 'Lubanas novads' }, { value: 'LV-058', country_code: 'LV', text: 'Ludzas novads' }, { value: 'LV-059', country_code: 'LV', text: 'Madonas novads' }, { value: 'LV-061', country_code: 'LV', text: 'Malpils novads' }, { value: 'LV-067', country_code: 'LV', text: 'Ogres novads' }, { value: 'LV-068', country_code: 'LV', text: 'Olaines novads' }, { value: 'LV-069', country_code: 'LV', text: 'Ozolnieku novads' }, { value: 'LV-073', country_code: 'LV', text: 'Preilu novads' }, { value: 'LV-077', country_code: 'LV', text: 'Rezeknes novads' }, { value: 'LV-RIX', country_code: 'LV', text: 'Riga' }, { value: 'LV-079', country_code: 'LV', text: 'Rojas novads' }, { value: 'LV-080', country_code: 'LV', text: 'Ropazu novads' }, { value: 'LV-082', country_code: 'LV', text: 'Rugaju novads' }, { value: 'LV-083', country_code: 'LV', text: 'Rundales novads' }, { value: 'LV-086', country_code: 'LV', text: 'Salacgrivas novads' }, { value: 'LV-088', country_code: 'LV', text: 'Saldus novads' }, { value: 'LV-090', country_code: 'LV', text: 'Sejas novads' }, { value: 'LV-091', country_code: 'LV', text: 'Siguldas novads' }, { value: 'LV-093', country_code: 'LV', text: 'Skrundas novads' }, { value: 'LV-095', country_code: 'LV', text: 'Stopinu novads' }, { value: 'LV-096', country_code: 'LV', text: 'Strencu novads' }, { value: 'LV-097', country_code: 'LV', text: 'Talsu novads' }, { value: 'LV-099', country_code: 'LV', text: 'Tukuma novads' }, { value: 'LV-100', country_code: 'LV', text: 'Vainodes novads' }, { value: 'LV-101', country_code: 'LV', text: 'Valkas novads' }, { value: 'LV-VMR', country_code: 'LV', text: 'Valmiera' }, { value: 'LV-103', country_code: 'LV', text: 'Varkavas novads' }, { value: 'LV-105', country_code: 'LV', text: 'Vecumnieku novads' }, { value: 'LV-106', country_code: 'LV', text: 'Ventspils novads' }, { value: 'LY-BU', country_code: 'LY', text: 'Al Butnan' }, { value: 'LY-JA', country_code: 'LY', text: 'Al Jabal al Akhdar' }, { value: 'LY-JG', country_code: 'LY', text: 'Al Jabal al Gharbi' }, { value: 'LY-JI', country_code: 'LY', text: 'Al Jafarah' }, { value: 'LY-JU', country_code: 'LY', text: 'Al Jufrah' }, { value: 'LY-KF', country_code: 'LY', text: 'Al Kufrah' }, { value: 'LY-MJ', country_code: 'LY', text: 'Al Marj' }, { value: 'LY-MB', country_code: 'LY', text: 'Al Marqab' }, { value: 'LY-WA', country_code: 'LY', text: 'Al Wahat' }, { value: 'LY-NQ', country_code: 'LY', text: 'An Nuqat al Khams' }, { value: 'LY-ZA', country_code: 'LY', text: 'Az Zawiyah' }, { value: 'LY-BA', country_code: 'LY', text: 'Banghazi' }, { value: 'LY-DR', country_code: 'LY', text: 'Darnah' }, { value: 'LY-GT', country_code: 'LY', text: 'Ghat' }, { value: 'LY-MI', country_code: 'LY', text: 'Misratah' }, { value: 'LY-MQ', country_code: 'LY', text: 'Murzuq' }, { value: 'LY-NL', country_code: 'LY', text: 'Nalut' }, { value: 'LY-SB', country_code: 'LY', text: 'Sabha' }, { value: 'LY-SR', country_code: 'LY', text: 'Surt' }, { value: 'LY-TB', country_code: 'LY', text: 'Tarabulus' }, { value: 'LY-WD', country_code: 'LY', text: 'Wadi al Hayat' }, { value: 'LY-WS', country_code: 'LY', text: "Wadi ash Shati'" }, { value: 'MA-09', country_code: 'MA', text: 'Chaouia-Ouardigha' }, { value: 'MA-10', country_code: 'MA', text: 'Doukhala-Abda' }, { value: 'MA-05', country_code: 'MA', text: 'Fes-Boulemane' }, { value: 'MA-02', country_code: 'MA', text: 'Gharb-Chrarda-Beni Hssen' }, { value: 'MA-08', country_code: 'MA', text: 'Grand Casablanca' }, { value: 'MA-14', country_code: 'MA', text: 'Guelmim-Es Semara' }, { value: 'MA-04', country_code: 'MA', text: "L'Oriental" }, { value: 'MA-11', country_code: 'MA', text: 'Marrakech-Tensift-Al Haouz' }, { value: 'MA-06', country_code: 'MA', text: 'Meknes-Tafilalet' }, { value: 'MA-07', country_code: 'MA', text: 'Rabat-Sale-Zemmour-Zaer' }, { value: 'MA-13', country_code: 'MA', text: 'Souss-Massa-Draa' }, { value: 'MA-12', country_code: 'MA', text: 'Tadla-Azilal' }, { value: 'MA-01', country_code: 'MA', text: 'Tanger-Tetouan' }, { value: 'MA-03', country_code: 'MA', text: 'Taza-Al Hoceima-Taounate' }, { value: 'MC-FO', country_code: 'MC', text: 'Fontvieille' }, { value: 'MC-CO', country_code: 'MC', text: 'La Condamine' }, { value: 'MC-MO', country_code: 'MC', text: 'Monaco-Ville' }, { value: 'MC-MG', country_code: 'MC', text: 'Moneghetti' }, { value: 'MC-MC', country_code: 'MC', text: 'Monte-Carlo' }, { value: 'MC-SR', country_code: 'MC', text: 'Saint-Roman' }, { value: 'MD-AN', country_code: 'MD', text: 'Anenii Noi' }, { value: 'MD-BA', country_code: 'MD', text: 'Balti' }, { value: 'MD-BS', country_code: 'MD', text: 'Basarabeasca' }, { value: 'MD-BD', country_code: 'MD', text: 'Bender' }, { value: 'MD-BR', country_code: 'MD', text: 'Briceni' }, { value: 'MD-CA', country_code: 'MD', text: 'Cahul' }, { value: 'MD-CL', country_code: 'MD', text: 'Calarasi' }, { value: 'MD-CT', country_code: 'MD', text: 'Cantemir' }, { value: 'MD-CS', country_code: 'MD', text: 'Causeni' }, { value: 'MD-CU', country_code: 'MD', text: 'Chisinau' }, { value: 'MD-CM', country_code: 'MD', text: 'Cimislia' }, { value: 'MD-CR', country_code: 'MD', text: 'Criuleni' }, { value: 'MD-DO', country_code: 'MD', text: 'Donduseni' }, { value: 'MD-DR', country_code: 'MD', text: 'Drochia' }, { value: 'MD-DU', country_code: 'MD', text: 'Dubasari' }, { value: 'MD-ED', country_code: 'MD', text: 'Edinet' }, { value: 'MD-FA', country_code: 'MD', text: 'Falesti' }, { value: 'MD-FL', country_code: 'MD', text: 'Floresti' }, { value: 'MD-GA', country_code: 'MD', text: 'Gagauzia, Unitatea teritoriala autonoma' }, { value: 'MD-GL', country_code: 'MD', text: 'Glodeni' }, { value: 'MD-HI', country_code: 'MD', text: 'Hincesti' }, { value: 'MD-IA', country_code: 'MD', text: 'Ialoveni' }, { value: 'MD-LE', country_code: 'MD', text: 'Leova' }, { value: 'MD-NI', country_code: 'MD', text: 'Nisporeni' }, { value: 'MD-OC', country_code: 'MD', text: 'Ocnita' }, { value: 'MD-OR', country_code: 'MD', text: 'Orhei' }, { value: 'MD-RE', country_code: 'MD', text: 'Rezina' }, { value: 'MD-RI', country_code: 'MD', text: 'Riscani' }, { value: 'MD-SI', country_code: 'MD', text: 'Singerei' }, { value: 'MD-SD', country_code: 'MD', text: 'Soldanesti' }, { value: 'MD-SO', country_code: 'MD', text: 'Soroca' }, { value: 'MD-SV', country_code: 'MD', text: 'Stefan Voda' }, { value: 'MD-SN', country_code: 'MD', text: 'Stinga Nistrului, unitatea teritoriala din' }, { value: 'MD-ST', country_code: 'MD', text: 'Straseni' }, { value: 'MD-TA', country_code: 'MD', text: 'Taraclia' }, { value: 'MD-TE', country_code: 'MD', text: 'Telenesti' }, { value: 'MD-UN', country_code: 'MD', text: 'Ungheni' }, { value: 'ME-02', country_code: 'ME', text: 'Bar' }, { value: 'ME-05', country_code: 'ME', text: 'Budva' }, { value: 'ME-06', country_code: 'ME', text: 'Cetinje' }, { value: 'ME-07', country_code: 'ME', text: 'Danilovgrad' }, { value: 'ME-08', country_code: 'ME', text: 'Herceg-Novi' }, { value: 'ME-09', country_code: 'ME', text: 'Kolasin' }, { value: 'ME-10', country_code: 'ME', text: 'Kotor' }, { value: 'ME-11', country_code: 'ME', text: 'Mojkovac' }, { value: 'ME-12', country_code: 'ME', text: 'Niksic' }, { value: 'ME-16', country_code: 'ME', text: 'Podgorica' }, { value: 'ME-19', country_code: 'ME', text: 'Tivat' }, { value: 'ME-20', country_code: 'ME', text: 'Ulcinj' }, { value: 'ME-21', country_code: 'ME', text: 'Zabljak' }, { value: 'MG-T', country_code: 'MG', text: 'Antananarivo' }, { value: 'MG-D', country_code: 'MG', text: 'Antsiranana' }, { value: 'MG-F', country_code: 'MG', text: 'Fianarantsoa' }, { value: 'MG-M', country_code: 'MG', text: 'Mahajanga' }, { value: 'MG-A', country_code: 'MG', text: 'Toamasina' }, { value: 'MG-U', country_code: 'MG', text: 'Toliara' }, { value: 'MH-ALL', country_code: 'MH', text: 'Ailinglaplap' }, { value: 'MH-ALK', country_code: 'MH', text: 'Ailuk' }, { value: 'MH-ARN', country_code: 'MH', text: 'Arno' }, { value: 'MH-AUR', country_code: 'MH', text: 'Aur' }, { value: 'MH-KIL', country_code: 'MH', text: 'Bikini and Kili' }, { value: 'MH-EBO', country_code: 'MH', text: 'Ebon' }, { value: 'MH-ENI', country_code: 'MH', text: 'Enewetak and Ujelang' }, { value: 'MH-JAB', country_code: 'MH', text: 'Jabat' }, { value: 'MH-JAL', country_code: 'MH', text: 'Jaluit' }, { value: 'MH-KWA', country_code: 'MH', text: 'Kwajalein' }, { value: 'MH-LAE', country_code: 'MH', text: 'Lae' }, { value: 'MH-LIB', country_code: 'MH', text: 'Lib' }, { value: 'MH-LIK', country_code: 'MH', text: 'Likiep' }, { value: 'MH-MAJ', country_code: 'MH', text: 'Majuro' }, { value: 'MH-MAL', country_code: 'MH', text: 'Maloelap' }, { value: 'MH-MEJ', country_code: 'MH', text: 'Mejit' }, { value: 'MH-MIL', country_code: 'MH', text: 'Mili' }, { value: 'MH-NMK', country_code: 'MH', text: 'Namdrik' }, { value: 'MH-NMU', country_code: 'MH', text: 'Namu' }, { value: 'MH-RON', country_code: 'MH', text: 'Rongelap' }, { value: 'MH-UJA', country_code: 'MH', text: 'Ujae' }, { value: 'MH-UTI', country_code: 'MH', text: 'Utrik' }, { value: 'MH-WTH', country_code: 'MH', text: 'Wotho' }, { value: 'MH-WTJ', country_code: 'MH', text: 'Wotje' }, { value: 'MK-02', country_code: 'MK', text: 'Aracinovo' }, { value: 'MK-03', country_code: 'MK', text: 'Berovo' }, { value: 'MK-04', country_code: 'MK', text: 'Bitola' }, { value: 'MK-05', country_code: 'MK', text: 'Bogdanci' }, { value: 'MK-06', country_code: 'MK', text: 'Bogovinje' }, { value: 'MK-07', country_code: 'MK', text: 'Bosilovo' }, { value: 'MK-08', country_code: 'MK', text: 'Brvenica' }, { value: 'MK-80', country_code: 'MK', text: 'Caska' }, { value: 'MK-78', country_code: 'MK', text: 'Centar Zupa' }, { value: 'MK-81', country_code: 'MK', text: 'Cesinovo-Oblesevo' }, { value: 'MK-82', country_code: 'MK', text: 'Cucer Sandevo' }, { value: 'MK-21', country_code: 'MK', text: 'Debar' }, { value: 'MK-22', country_code: 'MK', text: 'Debarca' }, { value: 'MK-23', country_code: 'MK', text: 'Delcevo' }, { value: 'MK-25', country_code: 'MK', text: 'Demir Hisar' }, { value: 'MK-24', country_code: 'MK', text: 'Demir Kapija' }, { value: 'MK-26', country_code: 'MK', text: 'Dojran' }, { value: 'MK-27', country_code: 'MK', text: 'Dolneni' }, { value: 'MK-18', country_code: 'MK', text: 'Gevgelija' }, { value: 'MK-19', country_code: 'MK', text: 'Gostivar' }, { value: 'MK-20', country_code: 'MK', text: 'Gradsko' }, { value: 'MK-34', country_code: 'MK', text: 'Ilinden' }, { value: 'MK-35', country_code: 'MK', text: 'Jegunovce' }, { value: 'MK-37', country_code: 'MK', text: 'Karbinci' }, { value: 'MK-36', country_code: 'MK', text: 'Kavadarci' }, { value: 'MK-40', country_code: 'MK', text: 'Kicevo' }, { value: 'MK-42', country_code: 'MK', text: 'Kocani' }, { value: 'MK-41', country_code: 'MK', text: 'Konce' }, { value: 'MK-43', country_code: 'MK', text: 'Kratovo' }, { value: 'MK-44', country_code: 'MK', text: 'Kriva Palanka' }, { value: 'MK-45', country_code: 'MK', text: 'Krivogastani' }, { value: 'MK-46', country_code: 'MK', text: 'Krusevo' }, { value: 'MK-47', country_code: 'MK', text: 'Kumanovo' }, { value: 'MK-48', country_code: 'MK', text: 'Lipkovo' }, { value: 'MK-49', country_code: 'MK', text: 'Lozovo' }, { value: 'MK-51', country_code: 'MK', text: 'Makedonska Kamenica' }, { value: 'MK-52', country_code: 'MK', text: 'Makedonski Brod' }, { value: 'MK-50', country_code: 'MK', text: 'Mavrovo i Rostusa' }, { value: 'MK-53', country_code: 'MK', text: 'Mogila' }, { value: 'MK-54', country_code: 'MK', text: 'Negotino' }, { value: 'MK-55', country_code: 'MK', text: 'Novaci' }, { value: 'MK-56', country_code: 'MK', text: 'Novo Selo' }, { value: 'MK-58', country_code: 'MK', text: 'Ohrid' }, { value: 'MK-60', country_code: 'MK', text: 'Pehcevo' }, { value: 'MK-59', country_code: 'MK', text: 'Petrovec' }, { value: 'MK-61', country_code: 'MK', text: 'Plasnica' }, { value: 'MK-62', country_code: 'MK', text: 'Prilep' }, { value: 'MK-63', country_code: 'MK', text: 'Probistip' }, { value: 'MK-64', country_code: 'MK', text: 'Radovis' }, { value: 'MK-65', country_code: 'MK', text: 'Rankovce' }, { value: 'MK-66', country_code: 'MK', text: 'Resen' }, { value: 'MK-67', country_code: 'MK', text: 'Rosoman' }, { value: 'MK-85', country_code: 'MK', text: 'Skopje' }, { value: 'MK-70', country_code: 'MK', text: 'Sopiste' }, { value: 'MK-71', country_code: 'MK', text: 'Staro Nagoricane' }, { value: 'MK-83', country_code: 'MK', text: 'Stip' }, { value: 'MK-72', country_code: 'MK', text: 'Struga' }, { value: 'MK-73', country_code: 'MK', text: 'Strumica' }, { value: 'MK-74', country_code: 'MK', text: 'Studenicani' }, { value: 'MK-69', country_code: 'MK', text: 'Sveti Nikole' }, { value: 'MK-75', country_code: 'MK', text: 'Tearce' }, { value: 'MK-76', country_code: 'MK', text: 'Tetovo' }, { value: 'MK-10', country_code: 'MK', text: 'Valandovo' }, { value: 'MK-11', country_code: 'MK', text: 'Vasilevo' }, { value: 'MK-13', country_code: 'MK', text: 'Veles' }, { value: 'MK-12', country_code: 'MK', text: 'Vevcani' }, { value: 'MK-14', country_code: 'MK', text: 'Vinica' }, { value: 'MK-16', country_code: 'MK', text: 'Vrapciste' }, { value: 'MK-32', country_code: 'MK', text: 'Zelenikovo' }, { value: 'MK-30', country_code: 'MK', text: 'Zelino' }, { value: 'MK-33', country_code: 'MK', text: 'Zrnovci' }, { value: 'ML-BKO', country_code: 'ML', text: 'Bamako' }, { value: 'ML-7', country_code: 'ML', text: 'Gao' }, { value: 'ML-1', country_code: 'ML', text: 'Kayes' }, { value: 'ML-8', country_code: 'ML', text: 'Kidal' }, { value: 'ML-2', country_code: 'ML', text: 'Koulikoro' }, { value: 'ML-5', country_code: 'ML', text: 'Mopti' }, { value: 'ML-4', country_code: 'ML', text: 'Segou' }, { value: 'ML-3', country_code: 'ML', text: 'Sikasso' }, { value: 'ML-6', country_code: 'ML', text: 'Tombouctou' }, { value: 'MM-07', country_code: 'MM', text: 'Ayeyarwady' }, { value: 'MM-02', country_code: 'MM', text: 'Bago' }, { value: 'MM-14', country_code: 'MM', text: 'Chin' }, { value: 'MM-11', country_code: 'MM', text: 'Kachin' }, { value: 'MM-12', country_code: 'MM', text: 'Kayah' }, { value: 'MM-13', country_code: 'MM', text: 'Kayin' }, { value: 'MM-03', country_code: 'MM', text: 'Magway' }, { value: 'MM-04', country_code: 'MM', text: 'Mandalay' }, { value: 'MM-15', country_code: 'MM', text: 'Mon' }, { value: 'MM-18', country_code: 'MM', text: 'Nay Pyi Taw' }, { value: 'MM-16', country_code: 'MM', text: 'Rakhine' }, { value: 'MM-01', country_code: 'MM', text: 'Sagaing' }, { value: 'MM-17', country_code: 'MM', text: 'Shan' }, { value: 'MM-05', country_code: 'MM', text: 'Tanintharyi' }, { value: 'MM-06', country_code: 'MM', text: 'Yangon' }, { value: 'MN-073', country_code: 'MN', text: 'Arhangay' }, { value: 'MN-071', country_code: 'MN', text: 'Bayan-Olgiy' }, { value: 'MN-069', country_code: 'MN', text: 'Bayanhongor' }, { value: 'MN-067', country_code: 'MN', text: 'Bulgan' }, { value: 'MN-037', country_code: 'MN', text: 'Darhan uul' }, { value: 'MN-061', country_code: 'MN', text: 'Dornod' }, { value: 'MN-063', country_code: 'MN', text: 'Dornogovi' }, { value: 'MN-059', country_code: 'MN', text: 'Dundgovi' }, { value: 'MN-057', country_code: 'MN', text: 'Dzavhan' }, { value: 'MN-065', country_code: 'MN', text: 'Govi-Altay' }, { value: 'MN-064', country_code: 'MN', text: 'Govi-Sumber' }, { value: 'MN-039', country_code: 'MN', text: 'Hentiy' }, { value: 'MN-043', country_code: 'MN', text: 'Hovd' }, { value: 'MN-041', country_code: 'MN', text: 'Hovsgol' }, { value: 'MN-053', country_code: 'MN', text: 'Omnogovi' }, { value: 'MN-035', country_code: 'MN', text: 'Orhon' }, { value: 'MN-055', country_code: 'MN', text: 'Ovorhangay' }, { value: 'MN-049', country_code: 'MN', text: 'Selenge' }, { value: 'MN-051', country_code: 'MN', text: 'Suhbaatar' }, { value: 'MN-047', country_code: 'MN', text: 'Tov' }, { value: 'MN-1', country_code: 'MN', text: 'Ulaanbaatar' }, { value: 'MN-046', country_code: 'MN', text: 'Uvs' }, { value: 'MR-07', country_code: 'MR', text: 'Adrar' }, { value: 'MR-03', country_code: 'MR', text: 'Assaba' }, { value: 'MR-05', country_code: 'MR', text: 'Brakna' }, { value: 'MR-08', country_code: 'MR', text: 'Dakhlet Nouadhibou' }, { value: 'MR-04', country_code: 'MR', text: 'Gorgol' }, { value: 'MR-10', country_code: 'MR', text: 'Guidimaka' }, { value: 'MR-01', country_code: 'MR', text: 'Hodh ech Chargui' }, { value: 'MR-02', country_code: 'MR', text: 'Hodh el Gharbi' }, { value: 'MR-12', country_code: 'MR', text: 'Inchiri' }, { value: 'MR-14', country_code: 'MR', text: 'Nouakchott Nord' }, { value: 'MR-09', country_code: 'MR', text: 'Tagant' }, { value: 'MR-11', country_code: 'MR', text: 'Tiris Zemmour' }, { value: 'MR-06', country_code: 'MR', text: 'Trarza' }, { value: 'MT-01', country_code: 'MT', text: 'Attard' }, { value: 'MT-02', country_code: 'MT', text: 'Balzan' }, { value: 'MT-04', country_code: 'MT', text: 'Birkirkara' }, { value: 'MT-05', country_code: 'MT', text: 'Birzebbuga' }, { value: 'MT-06', country_code: 'MT', text: 'Bormla' }, { value: 'MT-07', country_code: 'MT', text: 'Dingli' }, { value: 'MT-13', country_code: 'MT', text: 'Ghajnsielem' }, { value: 'MT-15', country_code: 'MT', text: 'Gharghur' }, { value: 'MT-17', country_code: 'MT', text: 'Ghaxaq' }, { value: 'MT-64', country_code: 'MT', text: 'Haz-Zabbar' }, { value: 'MT-60', country_code: 'MT', text: 'Valletta' }, { value: 'MT-03', country_code: 'MT', text: 'Birgu' }, { value: 'MT-08', country_code: 'MT', text: 'Fgura' }, { value: 'MT-09', country_code: 'MT', text: 'Floriana' }, { value: 'MT-11', country_code: 'MT', text: 'Gudja' }, { value: 'MT-18', country_code: 'MT', text: 'Hamrun' }, { value: 'MT-21', country_code: 'MT', text: 'Kalkara' }, { value: 'MT-26', country_code: 'MT', text: 'Marsa' }, { value: 'MT-30', country_code: 'MT', text: 'Mellieha' }, { value: 'MT-32', country_code: 'MT', text: 'Mosta' }, { value: 'MT-42', country_code: 'MT', text: 'Qala' }, { value: 'MT-44', country_code: 'MT', text: 'Qrendi' }, { value: 'MT-37', country_code: 'MT', text: 'Nadur' }, { value: 'MT-38', country_code: 'MT', text: 'Naxxar' }, { value: 'MT-46', country_code: 'MT', text: 'Rabat Malta' }, { value: 'MT-55', country_code: 'MT', text: 'Siggiewi' }, { value: 'MT-57', country_code: 'MT', text: 'Swieqi' }, { value: 'MT-61', country_code: 'MT', text: 'Xaghra' }, { value: 'MT-62', country_code: 'MT', text: 'Xewkija' }, { value: 'MT-65', country_code: 'MT', text: 'Zebbug Gozo' }, { value: 'MT-67', country_code: 'MT', text: 'Zejtun' }, { value: 'MT-68', country_code: 'MT', text: 'Zurrieq' }, { value: 'MT-23', country_code: 'MT', text: 'Kirkop' }, { value: 'MT-19', country_code: 'MT', text: 'Iklin' }, { value: 'MT-33', country_code: 'MT', text: 'Mqabba' }, { value: 'MT-34', country_code: 'MT', text: 'Msida' }, { value: 'MT-20', country_code: 'MT', text: 'Isla' }, { value: 'MT-24', country_code: 'MT', text: 'Lija' }, { value: 'MT-25', country_code: 'MT', text: 'Luqa' }, { value: 'MT-28', country_code: 'MT', text: 'Marsaxlokk' }, { value: 'MT-39', country_code: 'MT', text: 'Paola' }, { value: 'MT-43', country_code: 'MT', text: 'Qormi' }, { value: 'MT-47', country_code: 'MT', text: 'Safi' }, { value: 'MT-49', country_code: 'MT', text: 'Saint John' }, { value: 'MT-48', country_code: 'MT', text: 'Saint Julian' }, { value: 'MT-53', country_code: 'MT', text: 'Saint Lucia' }, { value: 'MT-51', country_code: 'MT', text: "Saint Paul's Bay" }, { value: 'MT-54', country_code: 'MT', text: 'Saint Venera' }, { value: 'MT-52', country_code: 'MT', text: 'Sannat' }, { value: 'MT-22', country_code: 'MT', text: 'Kercem' }, { value: 'MT-58', country_code: 'MT', text: "Ta' Xbiex" }, { value: 'MT-59', country_code: 'MT', text: 'Tarxien' }, { value: 'MT-56', country_code: 'MT', text: 'Sliema' }, { value: 'MT-45', country_code: 'MT', text: 'Rabat Gozo' }, { value: 'MU-BL', country_code: 'MU', text: 'Black River' }, { value: 'MU-FL', country_code: 'MU', text: 'Flacq' }, { value: 'MU-GP', country_code: 'MU', text: 'Grand Port' }, { value: 'MU-MO', country_code: 'MU', text: 'Moka' }, { value: 'MU-PA', country_code: 'MU', text: 'Pamplemousses' }, { value: 'MU-PW', country_code: 'MU', text: 'Plaines Wilhems' }, { value: 'MU-PU', country_code: 'MU', text: 'Port Louis' }, { value: 'MU-RR', country_code: 'MU', text: 'Riviere du Rempart' }, { value: 'MU-SA', country_code: 'MU', text: 'Savanne' }, { value: 'MV-02', country_code: 'MV', text: 'Alifu Alifu' }, { value: 'MV-20', country_code: 'MV', text: 'Baa' }, { value: 'MV-17', country_code: 'MV', text: 'Dhaalu' }, { value: 'MV-28', country_code: 'MV', text: 'Gaafu Dhaalu' }, { value: 'MV-07', country_code: 'MV', text: 'Haa Alifu' }, { value: 'MV-23', country_code: 'MV', text: 'Haa Dhaalu' }, { value: 'MV-26', country_code: 'MV', text: 'Kaafu' }, { value: 'MV-05', country_code: 'MV', text: 'Laamu' }, { value: 'MV-MLE', country_code: 'MV', text: 'Maale' }, { value: 'MV-12', country_code: 'MV', text: 'Meemu' }, { value: 'MV-25', country_code: 'MV', text: 'Noonu' }, { value: 'MV-13', country_code: 'MV', text: 'Raa' }, { value: 'MV-01', country_code: 'MV', text: 'Seenu' }, { value: 'MV-24', country_code: 'MV', text: 'Shaviyani' }, { value: 'MV-08', country_code: 'MV', text: 'Thaa' }, { value: 'MW-BA', country_code: 'MW', text: 'Balaka' }, { value: 'MW-BL', country_code: 'MW', text: 'Blantyre' }, { value: 'MW-CK', country_code: 'MW', text: 'Chikwawa' }, { value: 'MW-CR', country_code: 'MW', text: 'Chiradzulu' }, { value: 'MW-CT', country_code: 'MW', text: 'Chitipa' }, { value: 'MW-DE', country_code: 'MW', text: 'Dedza' }, { value: 'MW-DO', country_code: 'MW', text: 'Dowa' }, { value: 'MW-KR', country_code: 'MW', text: 'Karonga' }, { value: 'MW-KS', country_code: 'MW', text: 'Kasungu' }, { value: 'MW-LK', country_code: 'MW', text: 'Likoma' }, { value: 'MW-LI', country_code: 'MW', text: 'Lilongwe' }, { value: 'MW-MH', country_code: 'MW', text: 'Machinga' }, { value: 'MW-MG', country_code: 'MW', text: 'Mangochi' }, { value: 'MW-MC', country_code: 'MW', text: 'Mchinji' }, { value: 'MW-MU', country_code: 'MW', text: 'Mulanje' }, { value: 'MW-MW', country_code: 'MW', text: 'Mwanza' }, { value: 'MW-MZ', country_code: 'MW', text: 'Mzimba' }, { value: 'MW-NE', country_code: 'MW', text: 'Neno' }, { value: 'MW-NB', country_code: 'MW', text: 'Nkhata Bay' }, { value: 'MW-NK', country_code: 'MW', text: 'Nkhotakota' }, { value: 'MW-NS', country_code: 'MW', text: 'Nsanje' }, { value: 'MW-NU', country_code: 'MW', text: 'Ntcheu' }, { value: 'MW-NI', country_code: 'MW', text: 'Ntchisi' }, { value: 'MW-PH', country_code: 'MW', text: 'Phalombe' }, { value: 'MW-RU', country_code: 'MW', text: 'Rumphi' }, { value: 'MW-SA', country_code: 'MW', text: 'Salima' }, { value: 'MW-TH', country_code: 'MW', text: 'Thyolo' }, { value: 'MW-ZO', country_code: 'MW', text: 'Zomba' }, { value: 'MX-AGU', country_code: 'MX', text: 'Aguascalientes' }, { value: 'MX-BCN', country_code: 'MX', text: 'Baja California' }, { value: 'MX-BCS', country_code: 'MX', text: 'Baja California Sur' }, { value: 'MX-CAM', country_code: 'MX', text: 'Campeche' }, { value: 'MX-CHP', country_code: 'MX', text: 'Chiapas' }, { value: 'MX-CHH', country_code: 'MX', text: 'Chihuahua' }, { value: 'MX-CMX', country_code: 'MX', text: 'Ciudad de Mexico' }, { value: 'MX-COA', country_code: 'MX', text: 'Coahuila de Zaragoza' }, { value: 'MX-COL', country_code: 'MX', text: 'Colima' }, { value: 'MX-DUR', country_code: 'MX', text: 'Durango' }, { value: 'MX-GUA', country_code: 'MX', text: 'Guanajuato' }, { value: 'MX-GRO', country_code: 'MX', text: 'Guerrero' }, { value: 'MX-HID', country_code: 'MX', text: 'Hidalgo' }, { value: 'MX-JAL', country_code: 'MX', text: 'Jalisco' }, { value: 'MX-MEX', country_code: 'MX', text: 'Mexico' }, { value: 'MX-MIC', country_code: 'MX', text: 'Michoacan de Ocampo' }, { value: 'MX-MOR', country_code: 'MX', text: 'Morelos' }, { value: 'MX-NAY', country_code: 'MX', text: 'Nayarit' }, { value: 'MX-NLE', country_code: 'MX', text: 'Nuevo Leon' }, { value: 'MX-OAX', country_code: 'MX', text: 'Oaxaca' }, { value: 'MX-PUE', country_code: 'MX', text: 'Puebla' }, { value: 'MX-QUE', country_code: 'MX', text: 'Queretaro' }, { value: 'MX-ROO', country_code: 'MX', text: 'Quintana Roo' }, { value: 'MX-SLP', country_code: 'MX', text: 'San Luis Potosi' }, { value: 'MX-SIN', country_code: 'MX', text: 'Sinaloa' }, { value: 'MX-SON', country_code: 'MX', text: 'Sonora' }, { value: 'MX-TAB', country_code: 'MX', text: 'Tabasco' }, { value: 'MX-TAM', country_code: 'MX', text: 'Tamaulipas' }, { value: 'MX-TLA', country_code: 'MX', text: 'Tlaxcala' }, { value: 'MX-VER', country_code: 'MX', text: 'Veracruz de Ignacio de la Llave' }, { value: 'MX-YUC', country_code: 'MX', text: 'Yucatan' }, { value: 'MX-ZAC', country_code: 'MX', text: 'Zacatecas' }, { value: 'MY-01', country_code: 'MY', text: 'Johor' }, { value: 'MY-02', country_code: 'MY', text: 'Kedah' }, { value: 'MY-03', country_code: 'MY', text: 'Kelantan' }, { value: 'MY-04', country_code: 'MY', text: 'Melaka' }, { value: 'MY-05', country_code: 'MY', text: 'Negeri Sembilan' }, { value: 'MY-06', country_code: 'MY', text: 'Pahang' }, { value: 'MY-08', country_code: 'MY', text: 'Perak' }, { value: 'MY-09', country_code: 'MY', text: 'Perlis' }, { value: 'MY-07', country_code: 'MY', text: 'Pulau Pinang' }, { value: 'MY-12', country_code: 'MY', text: 'Sabah' }, { value: 'MY-13', country_code: 'MY', text: 'Sarawak' }, { value: 'MY-10', country_code: 'MY', text: 'Selangor' }, { value: 'MY-11', country_code: 'MY', text: 'Terengganu' }, { value: 'MY-14', country_code: 'MY', text: 'Wilayah Persekutuan Kuala Lumpur' }, { value: 'MY-15', country_code: 'MY', text: 'Wilayah Persekutuan Labuan' }, { value: 'MY-16', country_code: 'MY', text: 'Wilayah Persekutuan Putrajaya' }, { value: 'MZ-P', country_code: 'MZ', text: 'Cabo Delgado' }, { value: 'MZ-G', country_code: 'MZ', text: 'Gaza' }, { value: 'MZ-I', country_code: 'MZ', text: 'Inhambane' }, { value: 'MZ-B', country_code: 'MZ', text: 'Manica' }, { value: 'MZ-MPM', country_code: 'MZ', text: 'Maputo' }, { value: 'MZ-N', country_code: 'MZ', text: 'Nampula' }, { value: 'MZ-A', country_code: 'MZ', text: 'Niassa' }, { value: 'MZ-S', country_code: 'MZ', text: 'Sofala' }, { value: 'MZ-T', country_code: 'MZ', text: 'Tete' }, { value: 'MZ-Q', country_code: 'MZ', text: 'Zambezia' }, { value: 'NA-ER', country_code: 'NA', text: 'Erongo' }, { value: 'NA-HA', country_code: 'NA', text: 'Hardap' }, { value: 'NA-KA', country_code: 'NA', text: 'Karas' }, { value: 'NA-KE', country_code: 'NA', text: 'Kavango East' }, { value: 'NA-KH', country_code: 'NA', text: 'Khomas' }, { value: 'NA-KU', country_code: 'NA', text: 'Kunene' }, { value: 'NA-OW', country_code: 'NA', text: 'Ohangwena' }, { value: 'NA-OH', country_code: 'NA', text: 'Omaheke' }, { value: 'NA-OS', country_code: 'NA', text: 'Omusati' }, { value: 'NA-ON', country_code: 'NA', text: 'Oshana' }, { value: 'NA-OT', country_code: 'NA', text: 'Oshikoto' }, { value: 'NA-OD', country_code: 'NA', text: 'Otjozondjupa' }, { value: 'NA-CA', country_code: 'NA', text: 'Zambezi' }, { value: 'NE-1', country_code: 'NE', text: 'Agadez' }, { value: 'NE-2', country_code: 'NE', text: 'Diffa' }, { value: 'NE-3', country_code: 'NE', text: 'Dosso' }, { value: 'NE-4', country_code: 'NE', text: 'Maradi' }, { value: 'NE-8', country_code: 'NE', text: 'Niamey' }, { value: 'NE-5', country_code: 'NE', text: 'Tahoua' }, { value: 'NE-6', country_code: 'NE', text: 'Tillaberi' }, { value: 'NE-7', country_code: 'NE', text: 'Zinder' }, { value: 'NG-AB', country_code: 'NG', text: 'Abia' }, { value: 'NG-FC', country_code: 'NG', text: 'Abuja Federal Capital Territory' }, { value: 'NG-AD', country_code: 'NG', text: 'Adamawa' }, { value: 'NG-AK', country_code: 'NG', text: 'Akwa Ibom' }, { value: 'NG-AN', country_code: 'NG', text: 'Anambra' }, { value: 'NG-BA', country_code: 'NG', text: 'Bauchi' }, { value: 'NG-BY', country_code: 'NG', text: 'Bayelsa' }, { value: 'NG-BE', country_code: 'NG', text: 'Benue' }, { value: 'NG-BO', country_code: 'NG', text: 'Borno' }, { value: 'NG-CR', country_code: 'NG', text: 'Cross River' }, { value: 'NG-DE', country_code: 'NG', text: 'Delta' }, { value: 'NG-EB', country_code: 'NG', text: 'Ebonyi' }, { value: 'NG-ED', country_code: 'NG', text: 'Edo' }, { value: 'NG-EK', country_code: 'NG', text: 'Ekiti' }, { value: 'NG-EN', country_code: 'NG', text: 'Enugu' }, { value: 'NG-GO', country_code: 'NG', text: 'Gombe' }, { value: 'NG-IM', country_code: 'NG', text: 'Imo' }, { value: 'NG-JI', country_code: 'NG', text: 'Jigawa' }, { value: 'NG-KD', country_code: 'NG', text: 'Kaduna' }, { value: 'NG-KN', country_code: 'NG', text: 'Kano' }, { value: 'NG-KT', country_code: 'NG', text: 'Katsina' }, { value: 'NG-KE', country_code: 'NG', text: 'Kebbi' }, { value: 'NG-KO', country_code: 'NG', text: 'Kogi' }, { value: 'NG-KW', country_code: 'NG', text: 'Kwara' }, { value: 'NG-LA', country_code: 'NG', text: 'Lagos' }, { value: 'NG-NA', country_code: 'NG', text: 'Nasarawa' }, { value: 'NG-NI', country_code: 'NG', text: 'Niger' }, { value: 'NG-OG', country_code: 'NG', text: 'Ogun' }, { value: 'NG-ON', country_code: 'NG', text: 'Ondo' }, { value: 'NG-OS', country_code: 'NG', text: 'Osun' }, { value: 'NG-OY', country_code: 'NG', text: 'Oyo' }, { value: 'NG-PL', country_code: 'NG', text: 'Plateau' }, { value: 'NG-RI', country_code: 'NG', text: 'Rivers' }, { value: 'NG-SO', country_code: 'NG', text: 'Sokoto' }, { value: 'NG-TA', country_code: 'NG', text: 'Taraba' }, { value: 'NG-YO', country_code: 'NG', text: 'Yobe' }, { value: 'NG-ZA', country_code: 'NG', text: 'Zamfara' }, { value: 'NI-AN', country_code: 'NI', text: 'Atlantico Norte' }, { value: 'NI-AS', country_code: 'NI', text: 'Atlantico Sur' }, { value: 'NI-BO', country_code: 'NI', text: 'Boaco' }, { value: 'NI-CA', country_code: 'NI', text: 'Carazo' }, { value: 'NI-CI', country_code: 'NI', text: 'Chinandega' }, { value: 'NI-CO', country_code: 'NI', text: 'Chontales' }, { value: 'NI-ES', country_code: 'NI', text: 'Esteli' }, { value: 'NI-GR', country_code: 'NI', text: 'Granada' }, { value: 'NI-JI', country_code: 'NI', text: 'Jinotega' }, { value: 'NI-LE', country_code: 'NI', text: 'Leon' }, { value: 'NI-MD', country_code: 'NI', text: 'Madriz' }, { value: 'NI-MN', country_code: 'NI', text: 'Managua' }, { value: 'NI-MS', country_code: 'NI', text: 'Masaya' }, { value: 'NI-MT', country_code: 'NI', text: 'Matagalpa' }, { value: 'NI-NS', country_code: 'NI', text: 'Nueva Segovia' }, { value: 'NI-SJ', country_code: 'NI', text: 'Rio San Juan' }, { value: 'NI-RI', country_code: 'NI', text: 'Rivas' }, { value: 'NL-DR', country_code: 'NL', text: 'Drenthe' }, { value: 'NL-FL', country_code: 'NL', text: 'Flevoland' }, { value: 'NL-FR', country_code: 'NL', text: 'Fryslan' }, { value: 'NL-GE', country_code: 'NL', text: 'Gelderland' }, { value: 'NL-GR', country_code: 'NL', text: 'Groningen' }, { value: 'NL-LI', country_code: 'NL', text: 'Limburg' }, { value: 'NL-NB', country_code: 'NL', text: 'Noord-Brabant' }, { value: 'NL-NH', country_code: 'NL', text: 'Noord-Holland' }, { value: 'NL-OV', country_code: 'NL', text: 'Overijssel' }, { value: 'NL-UT', country_code: 'NL', text: 'Utrecht' }, { value: 'NL-ZE', country_code: 'NL', text: 'Zeeland' }, { value: 'NL-ZH', country_code: 'NL', text: 'Zuid-Holland' }, { value: 'NO-02', country_code: 'NO', text: 'Akershus' }, { value: 'NO-09', country_code: 'NO', text: 'Aust-Agder' }, { value: 'NO-06', country_code: 'NO', text: 'Buskerud' }, { value: 'NO-20', country_code: 'NO', text: 'Finnmark' }, { value: 'NO-04', country_code: 'NO', text: 'Hedmark' }, { value: 'NO-12', country_code: 'NO', text: 'Hordaland' }, { value: 'NO-15', country_code: 'NO', text: 'More og Romsdal' }, { value: 'NO-17', country_code: 'NO', text: 'Nord-Trondelag' }, { value: 'NO-18', country_code: 'NO', text: 'Nordland' }, { value: 'NO-05', country_code: 'NO', text: 'Oppland' }, { value: 'NO-03', country_code: 'NO', text: 'Oslo' }, { value: 'NO-01', country_code: 'NO', text: 'Ostfold' }, { value: 'NO-11', country_code: 'NO', text: 'Rogaland' }, { value: 'NO-14', country_code: 'NO', text: 'Sogn og Fjordane' }, { value: 'NO-16', country_code: 'NO', text: 'Sor-Trondelag' }, { value: 'NO-08', country_code: 'NO', text: 'Telemark' }, { value: 'NO-19', country_code: 'NO', text: 'Troms' }, { value: 'NO-10', country_code: 'NO', text: 'Vest-Agder' }, { value: 'NO-07', country_code: 'NO', text: 'Vestfold' }, { value: 'NP-BA', country_code: 'NP', text: 'Bagmati' }, { value: 'NP-BH', country_code: 'NP', text: 'Bheri' }, { value: 'NP-DH', country_code: 'NP', text: 'Dhawalagiri' }, { value: 'NP-GA', country_code: 'NP', text: 'Gandaki' }, { value: 'NP-JA', country_code: 'NP', text: 'Janakpur' }, { value: 'NP-KA', country_code: 'NP', text: 'Karnali' }, { value: 'NP-KO', country_code: 'NP', text: 'Kosi' }, { value: 'NP-LU', country_code: 'NP', text: 'Lumbini' }, { value: 'NP-MA', country_code: 'NP', text: 'Mahakali' }, { value: 'NP-ME', country_code: 'NP', text: 'Mechi' }, { value: 'NP-NA', country_code: 'NP', text: 'Narayani' }, { value: 'NP-RA', country_code: 'NP', text: 'Rapti' }, { value: 'NP-SA', country_code: 'NP', text: 'Sagarmatha' }, { value: 'NP-SE', country_code: 'NP', text: 'Seti' }, { value: 'NR-14', country_code: 'NR', text: 'Yaren' }, { value: 'NZ-AUK', country_code: 'NZ', text: 'Auckland' }, { value: 'NZ-BOP', country_code: 'NZ', text: 'Bay of Plenty' }, { value: 'NZ-CAN', country_code: 'NZ', text: 'Canterbury' }, { value: 'NZ-CIT', country_code: 'NZ', text: 'Chatham Islands Territory' }, { value: 'NZ-GIS', country_code: 'NZ', text: 'Gisborne' }, { value: 'NZ-HKB', country_code: 'NZ', text: "Hawke's Bay" }, { value: 'NZ-MWT', country_code: 'NZ', text: 'Manawatu-Wanganui' }, { value: 'NZ-MBH', country_code: 'NZ', text: 'Marlborough' }, { value: 'NZ-NSN', country_code: 'NZ', text: 'Nelson' }, { value: 'NZ-NTL', country_code: 'NZ', text: 'Northland' }, { value: 'NZ-OTA', country_code: 'NZ', text: 'Otago' }, { value: 'NZ-STL', country_code: 'NZ', text: 'Southland' }, { value: 'NZ-TKI', country_code: 'NZ', text: 'Taranaki' }, { value: 'NZ-TAS', country_code: 'NZ', text: 'Tasman' }, { value: 'NZ-WKO', country_code: 'NZ', text: 'Waikato' }, { value: 'NZ-WGN', country_code: 'NZ', text: 'Wellington' }, { value: 'NZ-WTC', country_code: 'NZ', text: 'West Coast' }, { value: 'OM-DA', country_code: 'OM', text: 'Ad Dakhiliyah' }, { value: 'OM-BU', country_code: 'OM', text: 'Al Buraymi' }, { value: 'OM-WU', country_code: 'OM', text: 'Al Wusta' }, { value: 'OM-ZA', country_code: 'OM', text: 'Az Zahirah' }, { value: 'OM-BJ', country_code: 'OM', text: 'Janub al Batinah' }, { value: 'OM-SJ', country_code: 'OM', text: 'Janub ash Sharqiyah' }, { value: 'OM-MA', country_code: 'OM', text: 'Masqat' }, { value: 'OM-MU', country_code: 'OM', text: 'Musandam' }, { value: 'OM-BS', country_code: 'OM', text: 'Shamal al Batinah' }, { value: 'OM-SS', country_code: 'OM', text: 'Shamal ash Sharqiyah' }, { value: 'OM-ZU', country_code: 'OM', text: 'Zufar' }, { value: 'PA-1', country_code: 'PA', text: 'Bocas del Toro' }, { value: 'PA-4', country_code: 'PA', text: 'Chiriqui' }, { value: 'PA-2', country_code: 'PA', text: 'Cocle' }, { value: 'PA-3', country_code: 'PA', text: 'Colon' }, { value: 'PA-5', country_code: 'PA', text: 'Darien' }, { value: 'PA-6', country_code: 'PA', text: 'Herrera' }, { value: 'PA-7', country_code: 'PA', text: 'Los Santos' }, { value: 'PA-8', country_code: 'PA', text: 'Panama' }, { value: 'PA-9', country_code: 'PA', text: 'Veraguas' }, { value: 'PE-AMA', country_code: 'PE', text: 'Amazonas' }, { value: 'PE-ANC', country_code: 'PE', text: 'Ancash' }, { value: 'PE-APU', country_code: 'PE', text: 'Apurimac' }, { value: 'PE-ARE', country_code: 'PE', text: 'Arequipa' }, { value: 'PE-AYA', country_code: 'PE', text: 'Ayacucho' }, { value: 'PE-CAJ', country_code: 'PE', text: 'Cajamarca' }, { value: 'PE-CUS', country_code: 'PE', text: 'Cusco' }, { value: 'PE-CAL', country_code: 'PE', text: 'El Callao' }, { value: 'PE-HUV', country_code: 'PE', text: 'Huancavelica' }, { value: 'PE-HUC', country_code: 'PE', text: 'Huanuco' }, { value: 'PE-ICA', country_code: 'PE', text: 'Ica' }, { value: 'PE-JUN', country_code: 'PE', text: 'Junin' }, { value: 'PE-LAL', country_code: 'PE', text: 'La Libertad' }, { value: 'PE-LAM', country_code: 'PE', text: 'Lambayeque' }, { value: 'PE-LIM', country_code: 'PE', text: 'Lima' }, { value: 'PE-LOR', country_code: 'PE', text: 'Loreto' }, { value: 'PE-MDD', country_code: 'PE', text: 'Madre de Dios' }, { value: 'PE-MOQ', country_code: 'PE', text: 'Moquegua' }, { value: 'PE-PAS', country_code: 'PE', text: 'Pasco' }, { value: 'PE-PIU', country_code: 'PE', text: 'Piura' }, { value: 'PE-PUN', country_code: 'PE', text: 'Puno' }, { value: 'PE-SAM', country_code: 'PE', text: 'San Martin' }, { value: 'PE-TAC', country_code: 'PE', text: 'Tacna' }, { value: 'PE-TUM', country_code: 'PE', text: 'Tumbes' }, { value: 'PE-UCA', country_code: 'PE', text: 'Ucayali' }, { value: 'PG-NSB', country_code: 'PG', text: 'Bougainville' }, { value: 'PG-CPK', country_code: 'PG', text: 'Chimbu' }, { value: 'PG-EBR', country_code: 'PG', text: 'East New Britain' }, { value: 'PG-ESW', country_code: 'PG', text: 'East Sepik' }, { value: 'PG-EHG', country_code: 'PG', text: 'Eastern Highlands' }, { value: 'PG-EPW', country_code: 'PG', text: 'Enga' }, { value: 'PG-GPK', country_code: 'PG', text: 'Gulf' }, { value: 'PG-MPM', country_code: 'PG', text: 'Madang' }, { value: 'PG-MRL', country_code: 'PG', text: 'Manus' }, { value: 'PG-MBA', country_code: 'PG', text: 'Milne Bay' }, { value: 'PG-MPL', country_code: 'PG', text: 'Morobe' }, { value: 'PG-NCD', country_code: 'PG', text: 'National Capital District (Port Moresby)' }, { value: 'PG-NIK', country_code: 'PG', text: 'New Ireland' }, { value: 'PG-NPP', country_code: 'PG', text: 'Northern' }, { value: 'PG-SHM', country_code: 'PG', text: 'Southern Highlands' }, { value: 'PG-WBK', country_code: 'PG', text: 'West New Britain' }, { value: 'PG-SAN', country_code: 'PG', text: 'West Sepik' }, { value: 'PG-WPD', country_code: 'PG', text: 'Western' }, { value: 'PG-WHM', country_code: 'PG', text: 'Western Highlands' }, { value: 'PH-ABR', country_code: 'PH', text: 'Abra' }, { value: 'PH-AGN', country_code: 'PH', text: 'Agusan del Norte' }, { value: 'PH-AGS', country_code: 'PH', text: 'Agusan del Sur' }, { value: 'PH-AKL', country_code: 'PH', text: 'Aklan' }, { value: 'PH-ALB', country_code: 'PH', text: 'Albay' }, { value: 'PH-ANT', country_code: 'PH', text: 'Antique' }, { value: 'PH-APA', country_code: 'PH', text: 'Apayao' }, { value: 'PH-AUR', country_code: 'PH', text: 'Aurora' }, { value: 'PH-BAS', country_code: 'PH', text: 'Basilan' }, { value: 'PH-BAN', country_code: 'PH', text: 'Bataan' }, { value: 'PH-BTN', country_code: 'PH', text: 'Batanes' }, { value: 'PH-BTG', country_code: 'PH', text: 'Batangas' }, { value: 'PH-BEN', country_code: 'PH', text: 'Benguet' }, { value: 'PH-BOH', country_code: 'PH', text: 'Bohol' }, { value: 'PH-BUK', country_code: 'PH', text: 'Bukidnon' }, { value: 'PH-BUL', country_code: 'PH', text: 'Bulacan' }, { value: 'PH-CAG', country_code: 'PH', text: 'Cagayan' }, { value: 'PH-CAN', country_code: 'PH', text: 'Camarines Norte' }, { value: 'PH-CAS', country_code: 'PH', text: 'Camarines Sur' }, { value: 'PH-CAM', country_code: 'PH', text: 'Camiguin' }, { value: 'PH-CAP', country_code: 'PH', text: 'Capiz' }, { value: 'PH-CAT', country_code: 'PH', text: 'Catanduanes' }, { value: 'PH-CAV', country_code: 'PH', text: 'Cavite' }, { value: 'PH-CEB', country_code: 'PH', text: 'Cebu' }, { value: 'PH-NCO', country_code: 'PH', text: 'Cotabato' }, { value: 'PH-DAS', country_code: 'PH', text: 'Davao del Sur' }, { value: 'PH-DAO', country_code: 'PH', text: 'Davao Oriental' }, { value: 'PH-EAS', country_code: 'PH', text: 'Eastern Samar' }, { value: 'PH-IFU', country_code: 'PH', text: 'Ifugao' }, { value: 'PH-ILN', country_code: 'PH', text: 'Ilocos Norte' }, { value: 'PH-ILS', country_code: 'PH', text: 'Ilocos Sur' }, { value: 'PH-ILI', country_code: 'PH', text: 'Iloilo' }, { value: 'PH-ISA', country_code: 'PH', text: 'Isabela' }, { value: 'PH-KAL', country_code: 'PH', text: 'Kalinga' }, { value: 'PH-LUN', country_code: 'PH', text: 'La Union' }, { value: 'PH-LAG', country_code: 'PH', text: 'Laguna' }, { value: 'PH-LAN', country_code: 'PH', text: 'Lanao del Norte' }, { value: 'PH-LAS', country_code: 'PH', text: 'Lanao del Sur' }, { value: 'PH-LEY', country_code: 'PH', text: 'Leyte' }, { value: 'PH-MAG', country_code: 'PH', text: 'Maguindanao' }, { value: 'PH-MAD', country_code: 'PH', text: 'Marinduque' }, { value: 'PH-MAS', country_code: 'PH', text: 'Masbate' }, { value: 'PH-MDC', country_code: 'PH', text: 'Mindoro Occidental' }, { value: 'PH-MDR', country_code: 'PH', text: 'Mindoro Oriental' }, { value: 'PH-MSC', country_code: 'PH', text: 'Misamis Occidental' }, { value: 'PH-MSR', country_code: 'PH', text: 'Misamis Oriental' }, { value: 'PH-MOU', country_code: 'PH', text: 'Mountain Province' }, { value: 'PH-00', country_code: 'PH', text: 'National Capital Region' }, { value: 'PH-NEC', country_code: 'PH', text: 'Negros Occidental' }, { value: 'PH-NER', country_code: 'PH', text: 'Negros Oriental' }, { value: 'PH-NSA', country_code: 'PH', text: 'Northern Samar' }, { value: 'PH-NUE', country_code: 'PH', text: 'Nueva Ecija' }, { value: 'PH-NUV', country_code: 'PH', text: 'Nueva Vizcaya' }, { value: 'PH-PLW', country_code: 'PH', text: 'Palawan' }, { value: 'PH-PAM', country_code: 'PH', text: 'Pampanga' }, { value: 'PH-PAN', country_code: 'PH', text: 'Pangasinan' }, { value: 'PH-QUE', country_code: 'PH', text: 'Quezon' }, { value: 'PH-QUI', country_code: 'PH', text: 'Quirino' }, { value: 'PH-RIZ', country_code: 'PH', text: 'Rizal' }, { value: 'PH-ROM', country_code: 'PH', text: 'Romblon' }, { value: 'PH-WSA', country_code: 'PH', text: 'Samar' }, { value: 'PH-SIG', country_code: 'PH', text: 'Siquijor' }, { value: 'PH-SOR', country_code: 'PH', text: 'Sorsogon' }, { value: 'PH-SCO', country_code: 'PH', text: 'South Cotabato' }, { value: 'PH-SLE', country_code: 'PH', text: 'Southern Leyte' }, { value: 'PH-SUK', country_code: 'PH', text: 'Sultan Kudarat' }, { value: 'PH-SLU', country_code: 'PH', text: 'Sulu' }, { value: 'PH-SUN', country_code: 'PH', text: 'Surigao del Norte' }, { value: 'PH-SUR', country_code: 'PH', text: 'Surigao del Sur' }, { value: 'PH-TAR', country_code: 'PH', text: 'Tarlac' }, { value: 'PH-TAW', country_code: 'PH', text: 'Tawi-Tawi' }, { value: 'PH-ZMB', country_code: 'PH', text: 'Zambales' }, { value: 'PH-ZAN', country_code: 'PH', text: 'Zamboanga del Norte' }, { value: 'PH-ZAS', country_code: 'PH', text: 'Zamboanga del Sur' }, { value: 'PK-JK', country_code: 'PK', text: 'Azad Kashmir' }, { value: 'PK-BA', country_code: 'PK', text: 'Balochistan' }, { value: 'PK-TA', country_code: 'PK', text: 'Federally Administered Tribal Areas' }, { value: 'PK-GB', country_code: 'PK', text: 'Gilgit-Baltistan' }, { value: 'PK-IS', country_code: 'PK', text: 'Islamabad' }, { value: 'PK-KP', country_code: 'PK', text: 'Khyber Pakhtunkhwa' }, { value: 'PK-PB', country_code: 'PK', text: 'Punjab' }, { value: 'PK-SD', country_code: 'PK', text: 'Sindh' }, { value: 'PL-DS', country_code: 'PL', text: 'Dolnoslaskie' }, { value: 'PL-KP', country_code: 'PL', text: 'Kujawsko-pomorskie' }, { value: 'PL-LD', country_code: 'PL', text: 'Lodzkie' }, { value: 'PL-LU', country_code: 'PL', text: 'Lubelskie' }, { value: 'PL-LB', country_code: 'PL', text: 'Lubuskie' }, { value: 'PL-MA', country_code: 'PL', text: 'Malopolskie' }, { value: 'PL-MZ', country_code: 'PL', text: 'Mazowieckie' }, { value: 'PL-OP', country_code: 'PL', text: 'Opolskie' }, { value: 'PL-PK', country_code: 'PL', text: 'Podkarpackie' }, { value: 'PL-PD', country_code: 'PL', text: 'Podlaskie' }, { value: 'PL-PM', country_code: 'PL', text: 'Pomorskie' }, { value: 'PL-SL', country_code: 'PL', text: 'Slaskie' }, { value: 'PL-SK', country_code: 'PL', text: 'Swietokrzyskie' }, { value: 'PL-WN', country_code: 'PL', text: 'Warminsko-mazurskie' }, { value: 'PL-WP', country_code: 'PL', text: 'Wielkopolskie' }, { value: 'PL-ZP', country_code: 'PL', text: 'Zachodniopomorskie' }, { value: 'PS-BTH', country_code: 'PS', text: 'Bethlehem' }, { value: 'PS-GZA', country_code: 'PS', text: 'Gaza' }, { value: 'PS-HBN', country_code: 'PS', text: 'Hebron' }, { value: 'PS-JEN', country_code: 'PS', text: 'Jenin' }, { value: 'PS-JRH', country_code: 'PS', text: 'Jericho and Al Aghwar' }, { value: 'PS-JEM', country_code: 'PS', text: 'Jerusalem' }, { value: 'PS-NBS', country_code: 'PS', text: 'Nablus' }, { value: 'PS-QQA', country_code: 'PS', text: 'Qalqilya' }, { value: 'PS-RBH', country_code: 'PS', text: 'Ramallah' }, { value: 'PS-SLT', country_code: 'PS', text: 'Salfit' }, { value: 'PS-TBS', country_code: 'PS', text: 'Tubas' }, { value: 'PS-TKM', country_code: 'PS', text: 'Tulkarm' }, { value: 'PT-01', country_code: 'PT', text: 'Aveiro' }, { value: 'PT-02', country_code: 'PT', text: 'Beja' }, { value: 'PT-03', country_code: 'PT', text: 'Braga' }, { value: 'PT-04', country_code: 'PT', text: 'Braganca' }, { value: 'PT-05', country_code: 'PT', text: 'Castelo Branco' }, { value: 'PT-06', country_code: 'PT', text: 'Coimbra' }, { value: 'PT-07', country_code: 'PT', text: 'Evora' }, { value: 'PT-08', country_code: 'PT', text: 'Faro' }, { value: 'PT-09', country_code: 'PT', text: 'Guarda' }, { value: 'PT-10', country_code: 'PT', text: 'Leiria' }, { value: 'PT-11', country_code: 'PT', text: 'Lisboa' }, { value: 'PT-12', country_code: 'PT', text: 'Portalegre' }, { value: 'PT-13', country_code: 'PT', text: 'Porto' }, { value: 'PT-30', country_code: 'PT', text: 'Regiao Autonoma da Madeira' }, { value: 'PT-20', country_code: 'PT', text: 'Regiao Autonoma dos Acores' }, { value: 'PT-14', country_code: 'PT', text: 'Santarem' }, { value: 'PT-15', country_code: 'PT', text: 'Setubal' }, { value: 'PT-16', country_code: 'PT', text: 'Viana do Castelo' }, { value: 'PT-17', country_code: 'PT', text: 'Vila Real' }, { value: 'PT-18', country_code: 'PT', text: 'Viseu' }, { value: 'PW-002', country_code: 'PW', text: 'Aimeliik' }, { value: 'PW-004', country_code: 'PW', text: 'Airai' }, { value: 'PW-010', country_code: 'PW', text: 'Angaur' }, { value: 'PW-100', country_code: 'PW', text: 'Kayangel' }, { value: 'PW-150', country_code: 'PW', text: 'Koror' }, { value: 'PW-212', country_code: 'PW', text: 'Melekeok' }, { value: 'PW-214', country_code: 'PW', text: 'Ngaraard' }, { value: 'PW-218', country_code: 'PW', text: 'Ngarchelong' }, { value: 'PW-222', country_code: 'PW', text: 'Ngardmau' }, { value: 'PW-224', country_code: 'PW', text: 'Ngatpang' }, { value: 'PW-228', country_code: 'PW', text: 'Ngiwal' }, { value: 'PW-350', country_code: 'PW', text: 'Peleliu' }, { value: 'PY-16', country_code: 'PY', text: 'Alto Paraguay' }, { value: 'PY-10', country_code: 'PY', text: 'Alto Parana' }, { value: 'PY-13', country_code: 'PY', text: 'Amambay' }, { value: 'PY-ASU', country_code: 'PY', text: 'Asuncion' }, { value: 'PY-19', country_code: 'PY', text: 'Boqueron' }, { value: 'PY-5', country_code: 'PY', text: 'Caaguazu' }, { value: 'PY-6', country_code: 'PY', text: 'Caazapa' }, { value: 'PY-14', country_code: 'PY', text: 'Canindeyu' }, { value: 'PY-11', country_code: 'PY', text: 'Central' }, { value: 'PY-1', country_code: 'PY', text: 'Concepcion' }, { value: 'PY-3', country_code: 'PY', text: 'Cordillera' }, { value: 'PY-4', country_code: 'PY', text: 'Guaira' }, { value: 'PY-7', country_code: 'PY', text: 'Itapua' }, { value: 'PY-8', country_code: 'PY', text: 'Misiones' }, { value: 'PY-12', country_code: 'PY', text: 'Neembucu' }, { value: 'PY-9', country_code: 'PY', text: 'Paraguari' }, { value: 'PY-15', country_code: 'PY', text: 'Presidente Hayes' }, { value: 'PY-2', country_code: 'PY', text: 'San Pedro' }, { value: 'QA-DA', country_code: 'QA', text: 'Ad Dawhah' }, { value: 'QA-KH', country_code: 'QA', text: 'Al Khawr wa adh Dhakhirah' }, { value: 'QA-WA', country_code: 'QA', text: 'Al Wakrah' }, { value: 'QA-RA', country_code: 'QA', text: 'Ar Rayyan' }, { value: 'QA-MS', country_code: 'QA', text: 'Ash Shamal' }, { value: 'QA-ZA', country_code: 'QA', text: "Az Za'ayin" }, { value: 'QA-US', country_code: 'QA', text: 'Umm Salal' }, { value: 'RO-AB', country_code: 'RO', text: 'Alba' }, { value: 'RO-AR', country_code: 'RO', text: 'Arad' }, { value: 'RO-AG', country_code: 'RO', text: 'Arges' }, { value: 'RO-BC', country_code: 'RO', text: 'Bacau' }, { value: 'RO-BH', country_code: 'RO', text: 'Bihor' }, { value: 'RO-BN', country_code: 'RO', text: 'Bistrita-Nasaud' }, { value: 'RO-BT', country_code: 'RO', text: 'Botosani' }, { value: 'RO-BR', country_code: 'RO', text: 'Braila' }, { value: 'RO-BV', country_code: 'RO', text: 'Brasov' }, { value: 'RO-B', country_code: 'RO', text: 'Bucuresti' }, { value: 'RO-BZ', country_code: 'RO', text: 'Buzau' }, { value: 'RO-CL', country_code: 'RO', text: 'Calarasi' }, { value: 'RO-CS', country_code: 'RO', text: 'Caras-Severin' }, { value: 'RO-CJ', country_code: 'RO', text: 'Cluj' }, { value: 'RO-CT', country_code: 'RO', text: 'Constanta' }, { value: 'RO-CV', country_code: 'RO', text: 'Covasna' }, { value: 'RO-DB', country_code: 'RO', text: 'Dambovita' }, { value: 'RO-DJ', country_code: 'RO', text: 'Dolj' }, { value: 'RO-GL', country_code: 'RO', text: 'Galati' }, { value: 'RO-GR', country_code: 'RO', text: 'Giurgiu' }, { value: 'RO-GJ', country_code: 'RO', text: 'Gorj' }, { value: 'RO-HR', country_code: 'RO', text: 'Harghita' }, { value: 'RO-HD', country_code: 'RO', text: 'Hunedoara' }, { value: 'RO-IL', country_code: 'RO', text: 'Ialomita' }, { value: 'RO-IS', country_code: 'RO', text: 'Iasi' }, { value: 'RO-IF', country_code: 'RO', text: 'Ilfov' }, { value: 'RO-MM', country_code: 'RO', text: 'Maramures' }, { value: 'RO-MH', country_code: 'RO', text: 'Mehedinti' }, { value: 'RO-MS', country_code: 'RO', text: 'Mures' }, { value: 'RO-NT', country_code: 'RO', text: 'Neamt' }, { value: 'RO-OT', country_code: 'RO', text: 'Olt' }, { value: 'RO-PH', country_code: 'RO', text: 'Prahova' }, { value: 'RO-SJ', country_code: 'RO', text: 'Salaj' }, { value: 'RO-SM', country_code: 'RO', text: 'Satu Mare' }, { value: 'RO-SB', country_code: 'RO', text: 'Sibiu' }, { value: 'RO-SV', country_code: 'RO', text: 'Suceava' }, { value: 'RO-TR', country_code: 'RO', text: 'Teleorman' }, { value: 'RO-TM', country_code: 'RO', text: 'Timis' }, { value: 'RO-TL', country_code: 'RO', text: 'Tulcea' }, { value: 'RO-VL', country_code: 'RO', text: 'Valcea' }, { value: 'RO-VS', country_code: 'RO', text: 'Vaslui' }, { value: 'RO-VN', country_code: 'RO', text: 'Vrancea' }, { value: 'RS-00', country_code: 'RS', text: 'Beograd' }, { value: 'RS-14', country_code: 'RS', text: 'Borski okrug' }, { value: 'RS-11', country_code: 'RS', text: 'Branicevski okrug' }, { value: 'RS-23', country_code: 'RS', text: 'Jablanicki okrug' }, { value: 'RS-06', country_code: 'RS', text: 'Juznobacki okrug' }, { value: 'RS-04', country_code: 'RS', text: 'Juznobanatski okrug' }, { value: 'RS-09', country_code: 'RS', text: 'Kolubarski okrug' }, { value: 'RS-28', country_code: 'RS', text: 'Kosovsko-Mitrovacki okrug' }, { value: 'RS-08', country_code: 'RS', text: 'Macvanski okrug' }, { value: 'RS-17', country_code: 'RS', text: 'Moravicki okrug' }, { value: 'RS-20', country_code: 'RS', text: 'Nisavski okrug' }, { value: 'RS-24', country_code: 'RS', text: 'Pcinjski okrug' }, { value: 'RS-26', country_code: 'RS', text: 'Pecki okrug' }, { value: 'RS-22', country_code: 'RS', text: 'Pirotski okrug' }, { value: 'RS-10', country_code: 'RS', text: 'Podunavski okrug' }, { value: 'RS-27', country_code: 'RS', text: 'Prizrenski okrug' }, { value: 'RS-19', country_code: 'RS', text: 'Rasinski okrug' }, { value: 'RS-18', country_code: 'RS', text: 'Raski okrug' }, { value: 'RS-01', country_code: 'RS', text: 'Severnobacki okrug' }, { value: 'RS-03', country_code: 'RS', text: 'Severnobanatski okrug' }, { value: 'RS-02', country_code: 'RS', text: 'Srednjebanatski okrug' }, { value: 'RS-07', country_code: 'RS', text: 'Sremski okrug' }, { value: 'RS-12', country_code: 'RS', text: 'Sumadijski okrug' }, { value: 'RS-21', country_code: 'RS', text: 'Toplicki okrug' }, { value: 'RS-15', country_code: 'RS', text: 'Zajecarski okrug' }, { value: 'RS-05', country_code: 'RS', text: 'Zapadnobacki okrug' }, { value: 'RS-16', country_code: 'RS', text: 'Zlatiborski okrug' }, { value: 'RU-AD', country_code: 'RU', text: 'Adygeya, Respublika' }, { value: 'RU-AL', country_code: 'RU', text: 'Altay, Respublika' }, { value: 'RU-ALT', country_code: 'RU', text: 'Altayskiy kray' }, { value: 'RU-AMU', country_code: 'RU', text: "Amurskaya oblast'" }, { value: 'RU-ARK', country_code: 'RU', text: "Arkhangel'skaya oblast'" }, { value: 'RU-AST', country_code: 'RU', text: "Astrakhanskaya oblast'" }, { value: 'RU-BA', country_code: 'RU', text: 'Bashkortostan, Respublika' }, { value: 'RU-BEL', country_code: 'RU', text: "Belgorodskaya oblast'" }, { value: 'RU-BRY', country_code: 'RU', text: "Bryanskaya oblast'" }, { value: 'RU-BU', country_code: 'RU', text: 'Buryatiya, Respublika' }, { value: 'RU-CE', country_code: 'RU', text: 'Chechenskaya Respublika' }, { value: 'RU-CHE', country_code: 'RU', text: "Chelyabinskaya oblast'" }, { value: 'RU-CHU', country_code: 'RU', text: 'Chukotskiy avtonomnyy okrug' }, { value: 'RU-CU', country_code: 'RU', text: 'Chuvashskaya Respublika' }, { value: 'RU-DA', country_code: 'RU', text: 'Dagestan, Respublika' }, { value: 'RU-IN', country_code: 'RU', text: 'Ingushetiya, Respublika' }, { value: 'RU-IRK', country_code: 'RU', text: "Irkutskaya oblast'" }, { value: 'RU-IVA', country_code: 'RU', text: "Ivanovskaya oblast'" }, { value: 'RU-KB', country_code: 'RU', text: 'Kabardino-Balkarskaya Respublika' }, { value: 'RU-KGD', country_code: 'RU', text: "Kaliningradskaya oblast'" }, { value: 'RU-KL', country_code: 'RU', text: 'Kalmykiya, Respublika' }, { value: 'RU-KLU', country_code: 'RU', text: "Kaluzhskaya oblast'" }, { value: 'RU-KAM', country_code: 'RU', text: 'Kamchatskiy kray' }, { value: 'RU-KC', country_code: 'RU', text: 'Karachayevo-Cherkesskaya Respublika' }, { value: 'RU-KR', country_code: 'RU', text: 'Kareliya, Respublika' }, { value: 'RU-KEM', country_code: 'RU', text: "Kemerovskaya oblast'" }, { value: 'RU-KHA', country_code: 'RU', text: 'Khabarovskiy kray' }, { value: 'RU-KK', country_code: 'RU', text: 'Khakasiya, Respublika' }, { value: 'RU-KHM', country_code: 'RU', text: 'Khanty-Mansiyskiy avtonomnyy okrug' }, { value: 'RU-KIR', country_code: 'RU', text: "Kirovskaya oblast'" }, { value: 'RU-KO', country_code: 'RU', text: 'Komi, Respublika' }, { value: 'RU-KOS', country_code: 'RU', text: "Kostromskaya oblast'" }, { value: 'RU-KDA', country_code: 'RU', text: 'Krasnodarskiy kray' }, { value: 'RU-KYA', country_code: 'RU', text: 'Krasnoyarskiy kray' }, { value: 'RU-KGN', country_code: 'RU', text: "Kurganskaya oblast'" }, { value: 'RU-KRS', country_code: 'RU', text: "Kurskaya oblast'" }, { value: 'RU-LEN', country_code: 'RU', text: "Leningradskaya oblast'" }, { value: 'RU-LIP', country_code: 'RU', text: "Lipetskaya oblast'" }, { value: 'RU-MAG', country_code: 'RU', text: "Magadanskaya oblast'" }, { value: 'RU-ME', country_code: 'RU', text: 'Mariy El, Respublika' }, { value: 'RU-MO', country_code: 'RU', text: 'Mordoviya, Respublika' }, { value: 'RU-MOS', country_code: 'RU', text: "Moskovskaya oblast'" }, { value: 'RU-MOW', country_code: 'RU', text: 'Moskva' }, { value: 'RU-MUR', country_code: 'RU', text: "Murmanskaya oblast'" }, { value: 'RU-NEN', country_code: 'RU', text: 'Nenetskiy avtonomnyy okrug' }, { value: 'RU-NIZ', country_code: 'RU', text: "Nizhegorodskaya oblast'" }, { value: 'RU-NGR', country_code: 'RU', text: "Novgorodskaya oblast'" }, { value: 'RU-NVS', country_code: 'RU', text: "Novosibirskaya oblast'" }, { value: 'RU-OMS', country_code: 'RU', text: "Omskaya oblast'" }, { value: 'RU-ORE', country_code: 'RU', text: "Orenburgskaya oblast'" }, { value: 'RU-ORL', country_code: 'RU', text: "Orlovskaya oblast'" }, { value: 'RU-PNZ', country_code: 'RU', text: "Penzenskaya oblast'" }, { value: 'RU-PER', country_code: 'RU', text: 'Permskiy kray' }, { value: 'RU-PRI', country_code: 'RU', text: 'Primorskiy kray' }, { value: 'RU-PSK', country_code: 'RU', text: "Pskovskaya oblast'" }, { value: 'RU-ROS', country_code: 'RU', text: "Rostovskaya oblast'" }, { value: 'RU-RYA', country_code: 'RU', text: "Ryazanskaya oblast'" }, { value: 'RU-SA', country_code: 'RU', text: 'Saha, Respublika' }, { value: 'RU-SAK', country_code: 'RU', text: "Sakhalinskaya oblast'" }, { value: 'RU-SAM', country_code: 'RU', text: "Samarskaya oblast'" }, { value: 'RU-SPE', country_code: 'RU', text: 'Sankt-Peterburg' }, { value: 'RU-SAR', country_code: 'RU', text: "Saratovskaya oblast'" }, { value: 'RU-SE', country_code: 'RU', text: 'Severnaya Osetiya, Respublika' }, { value: 'RU-SMO', country_code: 'RU', text: "Smolenskaya oblast'" }, { value: 'RU-STA', country_code: 'RU', text: "Stavropol'skiy kray" }, { value: 'RU-SVE', country_code: 'RU', text: "Sverdlovskaya oblast'" }, { value: 'RU-TAM', country_code: 'RU', text: "Tambovskaya oblast'" }, { value: 'RU-TA', country_code: 'RU', text: 'Tatarstan, Respublika' }, { value: 'RU-TOM', country_code: 'RU', text: "Tomskaya oblast'" }, { value: 'RU-TUL', country_code: 'RU', text: "Tul'skaya oblast'" }, { value: 'RU-TVE', country_code: 'RU', text: "Tverskaya oblast'" }, { value: 'RU-TYU', country_code: 'RU', text: "Tyumenskaya oblast'" }, { value: 'RU-TY', country_code: 'RU', text: 'Tyva, Respublika' }, { value: 'RU-UD', country_code: 'RU', text: 'Udmurtskaya Respublika' }, { value: 'RU-ULY', country_code: 'RU', text: "Ul'yanovskaya oblast'" }, { value: 'RU-VLA', country_code: 'RU', text: "Vladimirskaya oblast'" }, { value: 'RU-VGG', country_code: 'RU', text: "Volgogradskaya oblast'" }, { value: 'RU-VLG', country_code: 'RU', text: "Vologodskaya oblast'" }, { value: 'RU-VOR', country_code: 'RU', text: "Voronezhskaya oblast'" }, { value: 'RU-YAN', country_code: 'RU', text: 'Yamalo-Nenetskiy avtonomnyy okrug' }, { value: 'RU-YAR', country_code: 'RU', text: "Yaroslavskaya oblast'" }, { value: 'RU-YEV', country_code: 'RU', text: "Yevreyskaya avtonomnaya oblast'" }, { value: 'RU-ZAB', country_code: 'RU', text: "Zabaykal'skiy kray" }, { value: 'RW-02', country_code: 'RW', text: 'Est' }, { value: 'RW-03', country_code: 'RW', text: 'Nord' }, { value: 'RW-04', country_code: 'RW', text: 'Ouest' }, { value: 'RW-05', country_code: 'RW', text: 'Sud' }, { value: 'RW-01', country_code: 'RW', text: 'Ville de Kigali' }, { value: 'SA-14', country_code: 'SA', text: "'Asir" }, { value: 'SA-11', country_code: 'SA', text: 'Al Bahah' }, { value: 'SA-08', country_code: 'SA', text: 'Al Hudud ash Shamaliyah' }, { value: 'SA-12', country_code: 'SA', text: 'Al Jawf' }, { value: 'SA-03', country_code: 'SA', text: 'Al Madinah al Munawwarah' }, { value: 'SA-05', country_code: 'SA', text: 'Al Qasim' }, { value: 'SA-01', country_code: 'SA', text: 'Ar Riyad' }, { value: 'SA-04', country_code: 'SA', text: 'Ash Sharqiyah' }, { value: 'SA-06', country_code: 'SA', text: "Ha'il" }, { value: 'SA-09', country_code: 'SA', text: 'Jazan' }, { value: 'SA-02', country_code: 'SA', text: 'Makkah al Mukarramah' }, { value: 'SA-10', country_code: 'SA', text: 'Najran' }, { value: 'SA-07', country_code: 'SA', text: 'Tabuk' }, { value: 'SB-CE', country_code: 'SB', text: 'Central' }, { value: 'SB-GU', country_code: 'SB', text: 'Guadalcanal' }, { value: 'SB-IS', country_code: 'SB', text: 'Isabel' }, { value: 'SB-MK', country_code: 'SB', text: 'Makira-Ulawa' }, { value: 'SB-ML', country_code: 'SB', text: 'Malaita' }, { value: 'SB-WE', country_code: 'SB', text: 'Western' }, { value: 'SC-16', country_code: 'SC', text: 'English River' }, { value: 'SD-NB', country_code: 'SD', text: 'Blue Nile' }, { value: 'SD-GD', country_code: 'SD', text: 'Gedaref' }, { value: 'SD-GZ', country_code: 'SD', text: 'Gezira' }, { value: 'SD-KA', country_code: 'SD', text: 'Kassala' }, { value: 'SD-KH', country_code: 'SD', text: 'Khartoum' }, { value: 'SD-DN', country_code: 'SD', text: 'North Darfur' }, { value: 'SD-KN', country_code: 'SD', text: 'North Kordofan' }, { value: 'SD-NO', country_code: 'SD', text: 'Northern' }, { value: 'SD-RS', country_code: 'SD', text: 'Red Sea' }, { value: 'SD-NR', country_code: 'SD', text: 'River Nile' }, { value: 'SD-SI', country_code: 'SD', text: 'Sennar' }, { value: 'SD-DS', country_code: 'SD', text: 'South Darfur' }, { value: 'SD-KS', country_code: 'SD', text: 'South Kordofan' }, { value: 'SD-DW', country_code: 'SD', text: 'West Darfur' }, { value: 'SD-NW', country_code: 'SD', text: 'White Nile' }, { value: 'SE-K', country_code: 'SE', text: 'Blekinge lan' }, { value: 'SE-W', country_code: 'SE', text: 'Dalarnas lan' }, { value: 'SE-X', country_code: 'SE', text: 'Gavleborgs lan' }, { value: 'SE-I', country_code: 'SE', text: 'Gotlands lan' }, { value: 'SE-N', country_code: 'SE', text: 'Hallands lan' }, { value: 'SE-Z', country_code: 'SE', text: 'Jamtlands lan' }, { value: 'SE-F', country_code: 'SE', text: 'Jonkopings lan' }, { value: 'SE-H', country_code: 'SE', text: 'Kalmar lan' }, { value: 'SE-G', country_code: 'SE', text: 'Kronobergs lan' }, { value: 'SE-BD', country_code: 'SE', text: 'Norrbottens lan' }, { value: 'SE-T', country_code: 'SE', text: 'Orebro lan' }, { value: 'SE-E', country_code: 'SE', text: 'Ostergotlands lan' }, { value: 'SE-M', country_code: 'SE', text: 'Skane lan' }, { value: 'SE-D', country_code: 'SE', text: 'Sodermanlands lan' }, { value: 'SE-AB', country_code: 'SE', text: 'Stockholms lan' }, { value: 'SE-C', country_code: 'SE', text: 'Uppsala lan' }, { value: 'SE-S', country_code: 'SE', text: 'Varmlands lan' }, { value: 'SE-AC', country_code: 'SE', text: 'Vasterbottens lan' }, { value: 'SE-Y', country_code: 'SE', text: 'Vasternorrlands lan' }, { value: 'SE-U', country_code: 'SE', text: 'Vastmanlands lan' }, { value: 'SE-O', country_code: 'SE', text: 'Vastra Gotalands lan' }, { value: 'SH-AC', country_code: 'SH', text: 'Ascension' }, { value: 'SH-HL', country_code: 'SH', text: 'Saint Helena' }, { value: 'SH-TA', country_code: 'SH', text: 'Tristan da Cunha' }, { value: 'SI-001', country_code: 'SI', text: 'Ajdovscina' }, { value: 'SI-003', country_code: 'SI', text: 'Bled' }, { value: 'SI-004', country_code: 'SI', text: 'Bohinj' }, { value: 'SI-005', country_code: 'SI', text: 'Borovnica' }, { value: 'SI-006', country_code: 'SI', text: 'Bovec' }, { value: 'SI-009', country_code: 'SI', text: 'Brezice' }, { value: 'SI-008', country_code: 'SI', text: 'Brezovica' }, { value: 'SI-011', country_code: 'SI', text: 'Celje' }, { value: 'SI-013', country_code: 'SI', text: 'Cerknica' }, { value: 'SI-014', country_code: 'SI', text: 'Cerkno' }, { value: 'SI-015', country_code: 'SI', text: 'Crensovci' }, { value: 'SI-017', country_code: 'SI', text: 'Crnomelj' }, { value: 'SI-018', country_code: 'SI', text: 'Destrnik' }, { value: 'SI-019', country_code: 'SI', text: 'Divaca' }, { value: 'SI-023', country_code: 'SI', text: 'Domzale' }, { value: 'SI-025', country_code: 'SI', text: 'Dravograd' }, { value: 'SI-029', country_code: 'SI', text: 'Gornja Radgona' }, { value: 'SI-032', country_code: 'SI', text: 'Grosuplje' }, { value: 'SI-160', country_code: 'SI', text: 'Hoce-Slivnica' }, { value: 'SI-162', country_code: 'SI', text: 'Horjul' }, { value: 'SI-034', country_code: 'SI', text: 'Hrastnik' }, { value: 'SI-036', country_code: 'SI', text: 'Idrija' }, { value: 'SI-037', country_code: 'SI', text: 'Ig' }, { value: 'SI-038', country_code: 'SI', text: 'Ilirska Bistrica' }, { value: 'SI-039', country_code: 'SI', text: 'Ivancna Gorica' }, { value: 'SI-040', country_code: 'SI', text: 'Izola' }, { value: 'SI-041', country_code: 'SI', text: 'Jesenice' }, { value: 'SI-043', country_code: 'SI', text: 'Kamnik' }, { value: 'SI-044', country_code: 'SI', text: 'Kanal' }, { value: 'SI-045', country_code: 'SI', text: 'Kidricevo' }, { value: 'SI-046', country_code: 'SI', text: 'Kobarid' }, { value: 'SI-048', country_code: 'SI', text: 'Kocevje' }, { value: 'SI-050', country_code: 'SI', text: 'Koper' }, { value: 'SI-052', country_code: 'SI', text: 'Kranj' }, { value: 'SI-053', country_code: 'SI', text: 'Kranjska Gora' }, { value: 'SI-054', country_code: 'SI', text: 'Krsko' }, { value: 'SI-057', country_code: 'SI', text: 'Lasko' }, { value: 'SI-058', country_code: 'SI', text: 'Lenart' }, { value: 'SI-059', country_code: 'SI', text: 'Lendava' }, { value: 'SI-060', country_code: 'SI', text: 'Litija' }, { value: 'SI-061', country_code: 'SI', text: 'Ljubljana' }, { value: 'SI-063', country_code: 'SI', text: 'Ljutomer' }, { value: 'SI-208', country_code: 'SI', text: 'Log-Dragomer' }, { value: 'SI-064', country_code: 'SI', text: 'Logatec' }, { value: 'SI-167', country_code: 'SI', text: 'Lovrenc na Pohorju' }, { value: 'SI-070', country_code: 'SI', text: 'Maribor' }, { value: 'SI-071', country_code: 'SI', text: 'Medvode' }, { value: 'SI-072', country_code: 'SI', text: 'Menges' }, { value: 'SI-073', country_code: 'SI', text: 'Metlika' }, { value: 'SI-074', country_code: 'SI', text: 'Mezica' }, { value: 'SI-169', country_code: 'SI', text: 'Miklavz na Dravskem Polju' }, { value: 'SI-075', country_code: 'SI', text: 'Miren-Kostanjevica' }, { value: 'SI-076', country_code: 'SI', text: 'Mislinja' }, { value: 'SI-079', country_code: 'SI', text: 'Mozirje' }, { value: 'SI-080', country_code: 'SI', text: 'Murska Sobota' }, { value: 'SI-081', country_code: 'SI', text: 'Muta' }, { value: 'SI-084', country_code: 'SI', text: 'Nova Gorica' }, { value: 'SI-085', country_code: 'SI', text: 'Novo Mesto' }, { value: 'SI-086', country_code: 'SI', text: 'Odranci' }, { value: 'SI-171', country_code: 'SI', text: 'Oplotnica' }, { value: 'SI-087', country_code: 'SI', text: 'Ormoz' }, { value: 'SI-090', country_code: 'SI', text: 'Piran' }, { value: 'SI-091', country_code: 'SI', text: 'Pivka' }, { value: 'SI-200', country_code: 'SI', text: 'Poljcane' }, { value: 'SI-173', country_code: 'SI', text: 'Polzela' }, { value: 'SI-094', country_code: 'SI', text: 'Postojna' }, { value: 'SI-174', country_code: 'SI', text: 'Prebold' }, { value: 'SI-175', country_code: 'SI', text: 'Prevalje' }, { value: 'SI-096', country_code: 'SI', text: 'Ptuj' }, { value: 'SI-098', country_code: 'SI', text: 'Race-Fram' }, { value: 'SI-099', country_code: 'SI', text: 'Radece' }, { value: 'SI-100', country_code: 'SI', text: 'Radenci' }, { value: 'SI-101', country_code: 'SI', text: 'Radlje ob Dravi' }, { value: 'SI-102', country_code: 'SI', text: 'Radovljica' }, { value: 'SI-103', country_code: 'SI', text: 'Ravne na Koroskem' }, { value: 'SI-104', country_code: 'SI', text: 'Ribnica' }, { value: 'SI-106', country_code: 'SI', text: 'Rogaska Slatina' }, { value: 'SI-108', country_code: 'SI', text: 'Ruse' }, { value: 'SI-183', country_code: 'SI', text: 'Sempeter-Vrtojba' }, { value: 'SI-117', country_code: 'SI', text: 'Sencur' }, { value: 'SI-118', country_code: 'SI', text: 'Sentilj' }, { value: 'SI-120', country_code: 'SI', text: 'Sentjur' }, { value: 'SI-110', country_code: 'SI', text: 'Sevnica' }, { value: 'SI-111', country_code: 'SI', text: 'Sezana' }, { value: 'SI-122', country_code: 'SI', text: 'Skofja Loka' }, { value: 'SI-123', country_code: 'SI', text: 'Skofljica' }, { value: 'SI-112', country_code: 'SI', text: 'Slovenj Gradec' }, { value: 'SI-113', country_code: 'SI', text: 'Slovenska Bistrica' }, { value: 'SI-114', country_code: 'SI', text: 'Slovenske Konjice' }, { value: 'SI-126', country_code: 'SI', text: 'Sostanj' }, { value: 'SI-127', country_code: 'SI', text: 'Store' }, { value: 'SI-203', country_code: 'SI', text: 'Straza' }, { value: 'SI-128', country_code: 'SI', text: 'Tolmin' }, { value: 'SI-129', country_code: 'SI', text: 'Trbovlje' }, { value: 'SI-130', country_code: 'SI', text: 'Trebnje' }, { value: 'SI-131', country_code: 'SI', text: 'Trzic' }, { value: 'SI-186', country_code: 'SI', text: 'Trzin' }, { value: 'SI-132', country_code: 'SI', text: 'Turnisce' }, { value: 'SI-133', country_code: 'SI', text: 'Velenje' }, { value: 'SI-136', country_code: 'SI', text: 'Vipava' }, { value: 'SI-138', country_code: 'SI', text: 'Vodice' }, { value: 'SI-139', country_code: 'SI', text: 'Vojnik' }, { value: 'SI-140', country_code: 'SI', text: 'Vrhnika' }, { value: 'SI-141', country_code: 'SI', text: 'Vuzenica' }, { value: 'SI-142', country_code: 'SI', text: 'Zagorje ob Savi' }, { value: 'SI-190', country_code: 'SI', text: 'Zalec' }, { value: 'SI-146', country_code: 'SI', text: 'Zelezniki' }, { value: 'SI-147', country_code: 'SI', text: 'Ziri' }, { value: 'SI-144', country_code: 'SI', text: 'Zrece' }, { value: 'SI-193', country_code: 'SI', text: 'Zuzemberk' }, { value: 'SK-BC', country_code: 'SK', text: 'Banskobystricky kraj' }, { value: 'SK-BL', country_code: 'SK', text: 'Bratislavsky kraj' }, { value: 'SK-KI', country_code: 'SK', text: 'Kosicky kraj' }, { value: 'SK-NI', country_code: 'SK', text: 'Nitriansky kraj' }, { value: 'SK-PV', country_code: 'SK', text: 'Presovsky kraj' }, { value: 'SK-TC', country_code: 'SK', text: 'Trenciansky kraj' }, { value: 'SK-TA', country_code: 'SK', text: 'Trnavsky kraj' }, { value: 'SK-ZI', country_code: 'SK', text: 'Zilinsky kraj' }, { value: 'SL-E', country_code: 'SL', text: 'Eastern' }, { value: 'SL-N', country_code: 'SL', text: 'Northern' }, { value: 'SL-S', country_code: 'SL', text: 'Southern' }, { value: 'SL-W', country_code: 'SL', text: 'Western Area' }, { value: 'SM-01', country_code: 'SM', text: 'Acquaviva' }, { value: 'SM-02', country_code: 'SM', text: 'Chiesanuova' }, { value: 'SM-07', country_code: 'SM', text: 'San Marino' }, { value: 'SM-09', country_code: 'SM', text: 'Serravalle' }, { value: 'SN-DK', country_code: 'SN', text: 'Dakar' }, { value: 'SN-DB', country_code: 'SN', text: 'Diourbel' }, { value: 'SN-FK', country_code: 'SN', text: 'Fatick' }, { value: 'SN-KA', country_code: 'SN', text: 'Kaffrine' }, { value: 'SN-KL', country_code: 'SN', text: 'Kaolack' }, { value: 'SN-KE', country_code: 'SN', text: 'Kedougou' }, { value: 'SN-KD', country_code: 'SN', text: 'Kolda' }, { value: 'SN-LG', country_code: 'SN', text: 'Louga' }, { value: 'SN-MT', country_code: 'SN', text: 'Matam' }, { value: 'SN-SL', country_code: 'SN', text: 'Saint-Louis' }, { value: 'SN-SE', country_code: 'SN', text: 'Sedhiou' }, { value: 'SN-TC', country_code: 'SN', text: 'Tambacounda' }, { value: 'SN-TH', country_code: 'SN', text: 'Thies' }, { value: 'SN-ZG', country_code: 'SN', text: 'Ziguinchor' }, { value: 'SO-AW', country_code: 'SO', text: 'Awdal' }, { value: 'SO-BK', country_code: 'SO', text: 'Bakool' }, { value: 'SO-BN', country_code: 'SO', text: 'Banaadir' }, { value: 'SO-BR', country_code: 'SO', text: 'Bari' }, { value: 'SO-BY', country_code: 'SO', text: 'Bay' }, { value: 'SO-GA', country_code: 'SO', text: 'Galguduud' }, { value: 'SO-GE', country_code: 'SO', text: 'Gedo' }, { value: 'SO-HI', country_code: 'SO', text: 'Hiiraan' }, { value: 'SO-JD', country_code: 'SO', text: 'Jubbada Dhexe' }, { value: 'SO-JH', country_code: 'SO', text: 'Jubbada Hoose' }, { value: 'SO-MU', country_code: 'SO', text: 'Mudug' }, { value: 'SO-NU', country_code: 'SO', text: 'Nugaal' }, { value: 'SO-SA', country_code: 'SO', text: 'Sanaag' }, { value: 'SO-SD', country_code: 'SO', text: 'Shabeellaha Dhexe' }, { value: 'SO-SH', country_code: 'SO', text: 'Shabeellaha Hoose' }, { value: 'SO-SO', country_code: 'SO', text: 'Sool' }, { value: 'SO-TO', country_code: 'SO', text: 'Togdheer' }, { value: 'SO-WO', country_code: 'SO', text: 'Woqooyi Galbeed' }, { value: 'SR-BR', country_code: 'SR', text: 'Brokopondo' }, { value: 'SR-CM', country_code: 'SR', text: 'Commewijne' }, { value: 'SR-CR', country_code: 'SR', text: 'Coronie' }, { value: 'SR-MA', country_code: 'SR', text: 'Marowijne' }, { value: 'SR-NI', country_code: 'SR', text: 'Nickerie' }, { value: 'SR-PR', country_code: 'SR', text: 'Para' }, { value: 'SR-PM', country_code: 'SR', text: 'Paramaribo' }, { value: 'SR-SA', country_code: 'SR', text: 'Saramacca' }, { value: 'SR-WA', country_code: 'SR', text: 'Wanica' }, { value: 'SS-EC', country_code: 'SS', text: 'Central Equatoria' }, { value: 'SS-EE', country_code: 'SS', text: 'Eastern Equatoria' }, { value: 'SS-JG', country_code: 'SS', text: 'Jonglei' }, { value: 'SS-LK', country_code: 'SS', text: 'Lakes' }, { value: 'SS-BN', country_code: 'SS', text: 'Northern Bahr el Ghazal' }, { value: 'SS-UY', country_code: 'SS', text: 'Unity' }, { value: 'SS-NU', country_code: 'SS', text: 'Upper Nile' }, { value: 'SS-WR', country_code: 'SS', text: 'Warrap' }, { value: 'SS-BW', country_code: 'SS', text: 'Western Bahr el Ghazal' }, { value: 'SS-EW', country_code: 'SS', text: 'Western Equatoria' }, { value: 'ST-P', country_code: 'ST', text: 'Principe' }, { value: 'ST-S', country_code: 'ST', text: 'Sao Tome' }, { value: 'SV-AH', country_code: 'SV', text: 'Ahuachapan' }, { value: 'SV-CA', country_code: 'SV', text: 'Cabanas' }, { value: 'SV-CH', country_code: 'SV', text: 'Chalatenango' }, { value: 'SV-CU', country_code: 'SV', text: 'Cuscatlan' }, { value: 'SV-LI', country_code: 'SV', text: 'La Libertad' }, { value: 'SV-PA', country_code: 'SV', text: 'La Paz' }, { value: 'SV-UN', country_code: 'SV', text: 'La Union' }, { value: 'SV-MO', country_code: 'SV', text: 'Morazan' }, { value: 'SV-SM', country_code: 'SV', text: 'San Miguel' }, { value: 'SV-SS', country_code: 'SV', text: 'San Salvador' }, { value: 'SV-SV', country_code: 'SV', text: 'San Vicente' }, { value: 'SV-SA', country_code: 'SV', text: 'Santa Ana' }, { value: 'SV-SO', country_code: 'SV', text: 'Sonsonate' }, { value: 'SV-US', country_code: 'SV', text: 'Usulutan' }, { value: 'SY-HA', country_code: 'SY', text: 'Al Hasakah' }, { value: 'SY-LA', country_code: 'SY', text: 'Al Ladhiqiyah' }, { value: 'SY-QU', country_code: 'SY', text: 'Al Qunaytirah' }, { value: 'SY-RA', country_code: 'SY', text: 'Ar Raqqah' }, { value: 'SY-SU', country_code: 'SY', text: "As Suwayda'" }, { value: 'SY-DR', country_code: 'SY', text: "Dar'a" }, { value: 'SY-DY', country_code: 'SY', text: 'Dayr az Zawr' }, { value: 'SY-DI', country_code: 'SY', text: 'Dimashq' }, { value: 'SY-HL', country_code: 'SY', text: 'Halab' }, { value: 'SY-HM', country_code: 'SY', text: 'Hamah' }, { value: 'SY-HI', country_code: 'SY', text: 'Hims' }, { value: 'SY-ID', country_code: 'SY', text: 'Idlib' }, { value: 'SY-RD', country_code: 'SY', text: 'Rif Dimashq' }, { value: 'SY-TA', country_code: 'SY', text: 'Tartus' }, { value: 'SZ-HH', country_code: 'SZ', text: 'Hhohho' }, { value: 'SZ-LU', country_code: 'SZ', text: 'Lubombo' }, { value: 'SZ-MA', country_code: 'SZ', text: 'Manzini' }, { value: 'SZ-SH', country_code: 'SZ', text: 'Shiselweni' }, { value: 'TD-BG', country_code: 'TD', text: 'Bahr el Gazel' }, { value: 'TD-BA', country_code: 'TD', text: 'Batha' }, { value: 'TD-BO', country_code: 'TD', text: 'Borkou' }, { value: 'TD-CB', country_code: 'TD', text: 'Chari-Baguirmi' }, { value: 'TD-GR', country_code: 'TD', text: 'Guera' }, { value: 'TD-HL', country_code: 'TD', text: 'Hadjer Lamis' }, { value: 'TD-KA', country_code: 'TD', text: 'Kanem' }, { value: 'TD-LC', country_code: 'TD', text: 'Lac' }, { value: 'TD-LO', country_code: 'TD', text: 'Logone-Occidental' }, { value: 'TD-LR', country_code: 'TD', text: 'Logone-Oriental' }, { value: 'TD-MA', country_code: 'TD', text: 'Mandoul' }, { value: 'TD-ME', country_code: 'TD', text: 'Mayo-Kebbi-Est' }, { value: 'TD-MO', country_code: 'TD', text: 'Mayo-Kebbi-Ouest' }, { value: 'TD-MC', country_code: 'TD', text: 'Moyen-Chari' }, { value: 'TD-OD', country_code: 'TD', text: 'Ouaddai' }, { value: 'TD-SA', country_code: 'TD', text: 'Salamat' }, { value: 'TD-TA', country_code: 'TD', text: 'Tandjile' }, { value: 'TD-TI', country_code: 'TD', text: 'Tibesti' }, { value: 'TD-WF', country_code: 'TD', text: 'Wadi Fira' }, { value: 'TG-C', country_code: 'TG', text: 'Centrale' }, { value: 'TG-K', country_code: 'TG', text: 'Kara' }, { value: 'TG-M', country_code: 'TG', text: 'Maritime' }, { value: 'TG-P', country_code: 'TG', text: 'Plateaux' }, { value: 'TG-S', country_code: 'TG', text: 'Savannes' }, { value: 'TH-37', country_code: 'TH', text: 'Amnat Charoen' }, { value: 'TH-15', country_code: 'TH', text: 'Ang Thong' }, { value: 'TH-31', country_code: 'TH', text: 'Buri Ram' }, { value: 'TH-24', country_code: 'TH', text: 'Chachoengsao' }, { value: 'TH-18', country_code: 'TH', text: 'Chai Nat' }, { value: 'TH-36', country_code: 'TH', text: 'Chaiyaphum' }, { value: 'TH-22', country_code: 'TH', text: 'Chanthaburi' }, { value: 'TH-50', country_code: 'TH', text: 'Chiang Mai' }, { value: 'TH-57', country_code: 'TH', text: 'Chiang Rai' }, { value: 'TH-20', country_code: 'TH', text: 'Chon Buri' }, { value: 'TH-86', country_code: 'TH', text: 'Chumphon' }, { value: 'TH-46', country_code: 'TH', text: 'Kalasin' }, { value: 'TH-62', country_code: 'TH', text: 'Kamphaeng Phet' }, { value: 'TH-71', country_code: 'TH', text: 'Kanchanaburi' }, { value: 'TH-40', country_code: 'TH', text: 'Khon Kaen' }, { value: 'TH-81', country_code: 'TH', text: 'Krabi' }, { value: 'TH-10', country_code: 'TH', text: 'Krung Thep Maha Nakhon' }, { value: 'TH-52', country_code: 'TH', text: 'Lampang' }, { value: 'TH-51', country_code: 'TH', text: 'Lamphun' }, { value: 'TH-42', country_code: 'TH', text: 'Loei' }, { value: 'TH-16', country_code: 'TH', text: 'Lop Buri' }, { value: 'TH-58', country_code: 'TH', text: 'Mae Hong Son' }, { value: 'TH-44', country_code: 'TH', text: 'Maha Sarakham' }, { value: 'TH-49', country_code: 'TH', text: 'Mukdahan' }, { value: 'TH-26', country_code: 'TH', text: 'Nakhon Nayok' }, { value: 'TH-73', country_code: 'TH', text: 'Nakhon Pathom' }, { value: 'TH-48', country_code: 'TH', text: 'Nakhon Phanom' }, { value: 'TH-30', country_code: 'TH', text: 'Nakhon Ratchasima' }, { value: 'TH-60', country_code: 'TH', text: 'Nakhon Sawan' }, { value: 'TH-80', country_code: 'TH', text: 'Nakhon Si Thammarat' }, { value: 'TH-55', country_code: 'TH', text: 'Nan' }, { value: 'TH-96', country_code: 'TH', text: 'Narathiwat' }, { value: 'TH-39', country_code: 'TH', text: 'Nong Bua Lam Phu' }, { value: 'TH-43', country_code: 'TH', text: 'Nong Khai' }, { value: 'TH-12', country_code: 'TH', text: 'Nonthaburi' }, { value: 'TH-13', country_code: 'TH', text: 'Pathum Thani' }, { value: 'TH-94', country_code: 'TH', text: 'Pattani' }, { value: 'TH-82', country_code: 'TH', text: 'Phangnga' }, { value: 'TH-93', country_code: 'TH', text: 'Phatthalung' }, { value: 'TH-56', country_code: 'TH', text: 'Phayao' }, { value: 'TH-67', country_code: 'TH', text: 'Phetchabun' }, { value: 'TH-76', country_code: 'TH', text: 'Phetchaburi' }, { value: 'TH-66', country_code: 'TH', text: 'Phichit' }, { value: 'TH-65', country_code: 'TH', text: 'Phitsanulok' }, { value: 'TH-14', country_code: 'TH', text: 'Phra Nakhon Si Ayutthaya' }, { value: 'TH-54', country_code: 'TH', text: 'Phrae' }, { value: 'TH-83', country_code: 'TH', text: 'Phuket' }, { value: 'TH-25', country_code: 'TH', text: 'Prachin Buri' }, { value: 'TH-77', country_code: 'TH', text: 'Prachuap Khiri Khan' }, { value: 'TH-85', country_code: 'TH', text: 'Ranong' }, { value: 'TH-70', country_code: 'TH', text: 'Ratchaburi' }, { value: 'TH-21', country_code: 'TH', text: 'Rayong' }, { value: 'TH-45', country_code: 'TH', text: 'Roi Et' }, { value: 'TH-27', country_code: 'TH', text: 'Sa Kaeo' }, { value: 'TH-47', country_code: 'TH', text: 'Sakon Nakhon' }, { value: 'TH-11', country_code: 'TH', text: 'Samut Prakan' }, { value: 'TH-74', country_code: 'TH', text: 'Samut Sakhon' }, { value: 'TH-75', country_code: 'TH', text: 'Samut Songkhram' }, { value: 'TH-19', country_code: 'TH', text: 'Saraburi' }, { value: 'TH-91', country_code: 'TH', text: 'Satun' }, { value: 'TH-33', country_code: 'TH', text: 'Si Sa Ket' }, { value: 'TH-17', country_code: 'TH', text: 'Sing Buri' }, { value: 'TH-90', country_code: 'TH', text: 'Songkhla' }, { value: 'TH-64', country_code: 'TH', text: 'Sukhothai' }, { value: 'TH-72', country_code: 'TH', text: 'Suphan Buri' }, { value: 'TH-84', country_code: 'TH', text: 'Surat Thani' }, { value: 'TH-32', country_code: 'TH', text: 'Surin' }, { value: 'TH-63', country_code: 'TH', text: 'Tak' }, { value: 'TH-92', country_code: 'TH', text: 'Trang' }, { value: 'TH-23', country_code: 'TH', text: 'Trat' }, { value: 'TH-34', country_code: 'TH', text: 'Ubon Ratchathani' }, { value: 'TH-41', country_code: 'TH', text: 'Udon Thani' }, { value: 'TH-61', country_code: 'TH', text: 'Uthai Thani' }, { value: 'TH-53', country_code: 'TH', text: 'Uttaradit' }, { value: 'TH-95', country_code: 'TH', text: 'Yala' }, { value: 'TH-35', country_code: 'TH', text: 'Yasothon' }, { value: 'TJ-DU', country_code: 'TJ', text: 'Dushanbe' }, { value: 'TJ-KT', country_code: 'TJ', text: 'Khatlon' }, { value: 'TJ-GB', country_code: 'TJ', text: 'Kuhistoni Badakhshon' }, { value: 'TJ-RA', country_code: 'TJ', text: 'Nohiyahoi Tobei Jumhuri' }, { value: 'TJ-SU', country_code: 'TJ', text: 'Sughd' }, { value: 'TL-DI', country_code: 'TL', text: 'Dili' }, { value: 'TM-A', country_code: 'TM', text: 'Ahal' }, { value: 'TM-B', country_code: 'TM', text: 'Balkan' }, { value: 'TM-D', country_code: 'TM', text: 'Dasoguz' }, { value: 'TM-L', country_code: 'TM', text: 'Lebap' }, { value: 'TM-M', country_code: 'TM', text: 'Mary' }, { value: 'TN-31', country_code: 'TN', text: 'Beja' }, { value: 'TN-13', country_code: 'TN', text: 'Ben Arous' }, { value: 'TN-23', country_code: 'TN', text: 'Bizerte' }, { value: 'TN-81', country_code: 'TN', text: 'Gabes' }, { value: 'TN-71', country_code: 'TN', text: 'Gafsa' }, { value: 'TN-32', country_code: 'TN', text: 'Jendouba' }, { value: 'TN-41', country_code: 'TN', text: 'Kairouan' }, { value: 'TN-42', country_code: 'TN', text: 'Kasserine' }, { value: 'TN-73', country_code: 'TN', text: 'Kebili' }, { value: 'TN-12', country_code: 'TN', text: "L'Ariana" }, { value: 'TN-14', country_code: 'TN', text: 'La Manouba' }, { value: 'TN-33', country_code: 'TN', text: 'Le Kef' }, { value: 'TN-53', country_code: 'TN', text: 'Mahdia' }, { value: 'TN-82', country_code: 'TN', text: 'Medenine' }, { value: 'TN-52', country_code: 'TN', text: 'Monastir' }, { value: 'TN-21', country_code: 'TN', text: 'Nabeul' }, { value: 'TN-61', country_code: 'TN', text: 'Sfax' }, { value: 'TN-43', country_code: 'TN', text: 'Sidi Bouzid' }, { value: 'TN-34', country_code: 'TN', text: 'Siliana' }, { value: 'TN-51', country_code: 'TN', text: 'Sousse' }, { value: 'TN-83', country_code: 'TN', text: 'Tataouine' }, { value: 'TN-72', country_code: 'TN', text: 'Tozeur' }, { value: 'TN-11', country_code: 'TN', text: 'Tunis' }, { value: 'TN-22', country_code: 'TN', text: 'Zaghouan' }, { value: 'TO-02', country_code: 'TO', text: "Ha'apai" }, { value: 'TO-04', country_code: 'TO', text: 'Tongatapu' }, { value: 'TO-05', country_code: 'TO', text: "Vava'u" }, { value: 'TR-01', country_code: 'TR', text: 'Adana' }, { value: 'TR-02', country_code: 'TR', text: 'Adiyaman' }, { value: 'TR-03', country_code: 'TR', text: 'Afyonkarahisar' }, { value: 'TR-04', country_code: 'TR', text: 'Agri' }, { value: 'TR-68', country_code: 'TR', text: 'Aksaray' }, { value: 'TR-05', country_code: 'TR', text: 'Amasya' }, { value: 'TR-06', country_code: 'TR', text: 'Ankara' }, { value: 'TR-07', country_code: 'TR', text: 'Antalya' }, { value: 'TR-75', country_code: 'TR', text: 'Ardahan' }, { value: 'TR-08', country_code: 'TR', text: 'Artvin' }, { value: 'TR-09', country_code: 'TR', text: 'Aydin' }, { value: 'TR-10', country_code: 'TR', text: 'Balikesir' }, { value: 'TR-74', country_code: 'TR', text: 'Bartin' }, { value: 'TR-72', country_code: 'TR', text: 'Batman' }, { value: 'TR-69', country_code: 'TR', text: 'Bayburt' }, { value: 'TR-11', country_code: 'TR', text: 'Bilecik' }, { value: 'TR-12', country_code: 'TR', text: 'Bingol' }, { value: 'TR-13', country_code: 'TR', text: 'Bitlis' }, { value: 'TR-14', country_code: 'TR', text: 'Bolu' }, { value: 'TR-15', country_code: 'TR', text: 'Burdur' }, { value: 'TR-16', country_code: 'TR', text: 'Bursa' }, { value: 'TR-17', country_code: 'TR', text: 'Canakkale' }, { value: 'TR-18', country_code: 'TR', text: 'Cankiri' }, { value: 'TR-19', country_code: 'TR', text: 'Corum' }, { value: 'TR-20', country_code: 'TR', text: 'Denizli' }, { value: 'TR-21', country_code: 'TR', text: 'Diyarbakir' }, { value: 'TR-81', country_code: 'TR', text: 'Duzce' }, { value: 'TR-22', country_code: 'TR', text: 'Edirne' }, { value: 'TR-23', country_code: 'TR', text: 'Elazig' }, { value: 'TR-24', country_code: 'TR', text: 'Erzincan' }, { value: 'TR-25', country_code: 'TR', text: 'Erzurum' }, { value: 'TR-26', country_code: 'TR', text: 'Eskisehir' }, { value: 'TR-27', country_code: 'TR', text: 'Gaziantep' }, { value: 'TR-28', country_code: 'TR', text: 'Giresun' }, { value: 'TR-29', country_code: 'TR', text: 'Gumushane' }, { value: 'TR-30', country_code: 'TR', text: 'Hakkari' }, { value: 'TR-31', country_code: 'TR', text: 'Hatay' }, { value: 'TR-76', country_code: 'TR', text: 'Igdir' }, { value: 'TR-32', country_code: 'TR', text: 'Isparta' }, { value: 'TR-34', country_code: 'TR', text: 'Istanbul' }, { value: 'TR-35', country_code: 'TR', text: 'Izmir' }, { value: 'TR-46', country_code: 'TR', text: 'Kahramanmaras' }, { value: 'TR-78', country_code: 'TR', text: 'Karabuk' }, { value: 'TR-70', country_code: 'TR', text: 'Karaman' }, { value: 'TR-36', country_code: 'TR', text: 'Kars' }, { value: 'TR-37', country_code: 'TR', text: 'Kastamonu' }, { value: 'TR-38', country_code: 'TR', text: 'Kayseri' }, { value: 'TR-79', country_code: 'TR', text: 'Kilis' }, { value: 'TR-71', country_code: 'TR', text: 'Kirikkale' }, { value: 'TR-39', country_code: 'TR', text: 'Kirklareli' }, { value: 'TR-40', country_code: 'TR', text: 'Kirsehir' }, { value: 'TR-41', country_code: 'TR', text: 'Kocaeli' }, { value: 'TR-42', country_code: 'TR', text: 'Konya' }, { value: 'TR-43', country_code: 'TR', text: 'Kutahya' }, { value: 'TR-44', country_code: 'TR', text: 'Malatya' }, { value: 'TR-45', country_code: 'TR', text: 'Manisa' }, { value: 'TR-47', country_code: 'TR', text: 'Mardin' }, { value: 'TR-33', country_code: 'TR', text: 'Mersin' }, { value: 'TR-48', country_code: 'TR', text: 'Mugla' }, { value: 'TR-49', country_code: 'TR', text: 'Mus' }, { value: 'TR-50', country_code: 'TR', text: 'Nevsehir' }, { value: 'TR-51', country_code: 'TR', text: 'Nigde' }, { value: 'TR-52', country_code: 'TR', text: 'Ordu' }, { value: 'TR-80', country_code: 'TR', text: 'Osmaniye' }, { value: 'TR-53', country_code: 'TR', text: 'Rize' }, { value: 'TR-54', country_code: 'TR', text: 'Sakarya' }, { value: 'TR-55', country_code: 'TR', text: 'Samsun' }, { value: 'TR-63', country_code: 'TR', text: 'Sanliurfa' }, { value: 'TR-56', country_code: 'TR', text: 'Siirt' }, { value: 'TR-57', country_code: 'TR', text: 'Sinop' }, { value: 'TR-73', country_code: 'TR', text: 'Sirnak' }, { value: 'TR-58', country_code: 'TR', text: 'Sivas' }, { value: 'TR-59', country_code: 'TR', text: 'Tekirdag' }, { value: 'TR-60', country_code: 'TR', text: 'Tokat' }, { value: 'TR-61', country_code: 'TR', text: 'Trabzon' }, { value: 'TR-62', country_code: 'TR', text: 'Tunceli' }, { value: 'TR-64', country_code: 'TR', text: 'Usak' }, { value: 'TR-65', country_code: 'TR', text: 'Van' }, { value: 'TR-77', country_code: 'TR', text: 'Yalova' }, { value: 'TR-66', country_code: 'TR', text: 'Yozgat' }, { value: 'TR-67', country_code: 'TR', text: 'Zonguldak' }, { value: 'TT-ARI', country_code: 'TT', text: 'Arima' }, { value: 'TT-CHA', country_code: 'TT', text: 'Chaguanas' }, { value: 'TT-CTT', country_code: 'TT', text: 'Couva-Tabaquite-Talparo' }, { value: 'TT-DMN', country_code: 'TT', text: 'Diego Martin' }, { value: 'TT-MRC', country_code: 'TT', text: 'Mayaro-Rio Claro' }, { value: 'TT-PED', country_code: 'TT', text: 'Penal-Debe' }, { value: 'TT-PTF', country_code: 'TT', text: 'Point Fortin' }, { value: 'TT-POS', country_code: 'TT', text: 'Port of Spain' }, { value: 'TT-PRT', country_code: 'TT', text: 'Princes Town' }, { value: 'TT-SFO', country_code: 'TT', text: 'San Fernando' }, { value: 'TT-SJL', country_code: 'TT', text: 'San Juan-Laventille' }, { value: 'TT-SGE', country_code: 'TT', text: 'Sangre Grande' }, { value: 'TT-SIP', country_code: 'TT', text: 'Siparia' }, { value: 'TT-TOB', country_code: 'TT', text: 'Tobago' }, { value: 'TT-TUP', country_code: 'TT', text: 'Tunapuna-Piarco' }, { value: 'TV-FUN', country_code: 'TV', text: 'Funafuti' }, { value: 'TW-CHA', country_code: 'TW', text: 'Changhua' }, { value: 'TW-CYQ', country_code: 'TW', text: 'Chiayi' }, { value: 'TW-HSQ', country_code: 'TW', text: 'Hsinchu' }, { value: 'TW-HUA', country_code: 'TW', text: 'Hualien' }, { value: 'TW-KHH', country_code: 'TW', text: 'Kaohsiung' }, { value: 'TW-KEE', country_code: 'TW', text: 'Keelung' }, { value: 'TW-KIN', country_code: 'TW', text: 'Kinmen' }, { value: 'TW-LIE', country_code: 'TW', text: 'Lienchiang' }, { value: 'TW-MIA', country_code: 'TW', text: 'Miaoli' }, { value: 'TW-NAN', country_code: 'TW', text: 'Nantou' }, { value: 'TW-NWT', country_code: 'TW', text: 'New Taipei' }, { value: 'TW-PEN', country_code: 'TW', text: 'Penghu' }, { value: 'TW-PIF', country_code: 'TW', text: 'Pingtung' }, { value: 'TW-TXG', country_code: 'TW', text: 'Taichung' }, { value: 'TW-TNN', country_code: 'TW', text: 'Tainan' }, { value: 'TW-TPE', country_code: 'TW', text: 'Taipei' }, { value: 'TW-TTT', country_code: 'TW', text: 'Taitung' }, { value: 'TW-TAO', country_code: 'TW', text: 'Taoyuan' }, { value: 'TW-ILA', country_code: 'TW', text: 'Yilan' }, { value: 'TW-YUN', country_code: 'TW', text: 'Yunlin' }, { value: 'TZ-01', country_code: 'TZ', text: 'Arusha' }, { value: 'TZ-02', country_code: 'TZ', text: 'Dar es Salaam' }, { value: 'TZ-03', country_code: 'TZ', text: 'Dodoma' }, { value: 'TZ-04', country_code: 'TZ', text: 'Iringa' }, { value: 'TZ-05', country_code: 'TZ', text: 'Kagera' }, { value: 'TZ-06', country_code: 'TZ', text: 'Kaskazini Pemba' }, { value: 'TZ-07', country_code: 'TZ', text: 'Kaskazini Unguja' }, { value: 'TZ-08', country_code: 'TZ', text: 'Kigoma' }, { value: 'TZ-09', country_code: 'TZ', text: 'Kilimanjaro' }, { value: 'TZ-10', country_code: 'TZ', text: 'Kusini Pemba' }, { value: 'TZ-11', country_code: 'TZ', text: 'Kusini Unguja' }, { value: 'TZ-12', country_code: 'TZ', text: 'Lindi' }, { value: 'TZ-26', country_code: 'TZ', text: 'Manyara' }, { value: 'TZ-13', country_code: 'TZ', text: 'Mara' }, { value: 'TZ-14', country_code: 'TZ', text: 'Mbeya' }, { value: 'TZ-15', country_code: 'TZ', text: 'Mjini Magharibi' }, { value: 'TZ-16', country_code: 'TZ', text: 'Morogoro' }, { value: 'TZ-17', country_code: 'TZ', text: 'Mtwara' }, { value: 'TZ-18', country_code: 'TZ', text: 'Mwanza' }, { value: 'TZ-19', country_code: 'TZ', text: 'Pwani' }, { value: 'TZ-20', country_code: 'TZ', text: 'Rukwa' }, { value: 'TZ-21', country_code: 'TZ', text: 'Ruvuma' }, { value: 'TZ-22', country_code: 'TZ', text: 'Shinyanga' }, { value: 'TZ-23', country_code: 'TZ', text: 'Singida' }, { value: 'TZ-24', country_code: 'TZ', text: 'Tabora' }, { value: 'TZ-25', country_code: 'TZ', text: 'Tanga' }, { value: 'UA-43', country_code: 'UA', text: 'Avtonomna Respublika Krym' }, { value: 'UA-71', country_code: 'UA', text: 'Cherkaska oblast' }, { value: 'UA-74', country_code: 'UA', text: 'Chernihivska oblast' }, { value: 'UA-77', country_code: 'UA', text: 'Chernivetska oblast' }, { value: 'UA-12', country_code: 'UA', text: 'Dnipropetrovska oblast' }, { value: 'UA-14', country_code: 'UA', text: 'Donetska oblast' }, { value: 'UA-26', country_code: 'UA', text: 'Ivano-Frankivska oblast' }, { value: 'UA-63', country_code: 'UA', text: 'Kharkivska oblast' }, { value: 'UA-65', country_code: 'UA', text: 'Khersonska oblast' }, { value: 'UA-68', country_code: 'UA', text: 'Khmelnytska oblast' }, { value: 'UA-35', country_code: 'UA', text: 'Kirovohradska oblast' }, { value: 'UA-30', country_code: 'UA', text: 'Kyiv' }, { value: 'UA-32', country_code: 'UA', text: 'Kyivska oblast' }, { value: 'UA-09', country_code: 'UA', text: 'Luhanska oblast' }, { value: 'UA-46', country_code: 'UA', text: 'Lvivska oblast' }, { value: 'UA-48', country_code: 'UA', text: 'Mykolaivska oblast' }, { value: 'UA-51', country_code: 'UA', text: 'Odeska oblast' }, { value: 'UA-53', country_code: 'UA', text: 'Poltavska oblast' }, { value: 'UA-56', country_code: 'UA', text: 'Rivnenska oblast' }, { value: 'UA-40', country_code: 'UA', text: "Sevastopol'" }, { value: 'UA-59', country_code: 'UA', text: 'Sumska oblast' }, { value: 'UA-61', country_code: 'UA', text: 'Ternopilska oblast' }, { value: 'UA-05', country_code: 'UA', text: 'Vinnytska oblast' }, { value: 'UA-07', country_code: 'UA', text: 'Volynska oblast' }, { value: 'UA-21', country_code: 'UA', text: 'Zakarpatska oblast' }, { value: 'UA-23', country_code: 'UA', text: 'Zaporizka oblast' }, { value: 'UA-18', country_code: 'UA', text: 'Zhytomyrska oblast' }, { value: 'UG-317', country_code: 'UG', text: 'Abim' }, { value: 'UG-301', country_code: 'UG', text: 'Adjumani' }, { value: 'UG-322', country_code: 'UG', text: 'Agago' }, { value: 'UG-323', country_code: 'UG', text: 'Alebtong' }, { value: 'UG-314', country_code: 'UG', text: 'Amolatar' }, { value: 'UG-324', country_code: 'UG', text: 'Amudat' }, { value: 'UG-216', country_code: 'UG', text: 'Amuria' }, { value: 'UG-319', country_code: 'UG', text: 'Amuru' }, { value: 'UG-302', country_code: 'UG', text: 'Apac' }, { value: 'UG-303', country_code: 'UG', text: 'Arua' }, { value: 'UG-217', country_code: 'UG', text: 'Budaka' }, { value: 'UG-223', country_code: 'UG', text: 'Bududa' }, { value: 'UG-201', country_code: 'UG', text: 'Bugiri' }, { value: 'UG-117', country_code: 'UG', text: 'Buikwe' }, { value: 'UG-224', country_code: 'UG', text: 'Bukedea' }, { value: 'UG-118', country_code: 'UG', text: 'Bukomansibi' }, { value: 'UG-218', country_code: 'UG', text: 'Bukwa' }, { value: 'UG-225', country_code: 'UG', text: 'Bulambuli' }, { value: 'UG-419', country_code: 'UG', text: 'Buliisa' }, { value: 'UG-401', country_code: 'UG', text: 'Bundibugyo' }, { value: 'UG-402', country_code: 'UG', text: 'Bushenyi' }, { value: 'UG-202', country_code: 'UG', text: 'Busia' }, { value: 'UG-219', country_code: 'UG', text: 'Butaleja' }, { value: 'UG-120', country_code: 'UG', text: 'Buvuma' }, { value: 'UG-226', country_code: 'UG', text: 'Buyende' }, { value: 'UG-318', country_code: 'UG', text: 'Dokolo' }, { value: 'UG-121', country_code: 'UG', text: 'Gomba' }, { value: 'UG-304', country_code: 'UG', text: 'Gulu' }, { value: 'UG-403', country_code: 'UG', text: 'Hoima' }, { value: 'UG-203', country_code: 'UG', text: 'Iganga' }, { value: 'UG-417', country_code: 'UG', text: 'Isingiro' }, { value: 'UG-204', country_code: 'UG', text: 'Jinja' }, { value: 'UG-315', country_code: 'UG', text: 'Kaabong' }, { value: 'UG-404', country_code: 'UG', text: 'Kabale' }, { value: 'UG-405', country_code: 'UG', text: 'Kabarole' }, { value: 'UG-213', country_code: 'UG', text: 'Kaberamaido' }, { value: 'UG-101', country_code: 'UG', text: 'Kalangala' }, { value: 'UG-220', country_code: 'UG', text: 'Kaliro' }, { value: 'UG-102', country_code: 'UG', text: 'Kampala' }, { value: 'UG-205', country_code: 'UG', text: 'Kamuli' }, { value: 'UG-413', country_code: 'UG', text: 'Kamwenge' }, { value: 'UG-414', country_code: 'UG', text: 'Kanungu' }, { value: 'UG-206', country_code: 'UG', text: 'Kapchorwa' }, { value: 'UG-406', country_code: 'UG', text: 'Kasese' }, { value: 'UG-207', country_code: 'UG', text: 'Katakwi' }, { value: 'UG-112', country_code: 'UG', text: 'Kayunga' }, { value: 'UG-407', country_code: 'UG', text: 'Kibaale' }, { value: 'UG-103', country_code: 'UG', text: 'Kiboga' }, { value: 'UG-227', country_code: 'UG', text: 'Kibuku' }, { value: 'UG-420', country_code: 'UG', text: 'Kiryandongo' }, { value: 'UG-408', country_code: 'UG', text: 'Kisoro' }, { value: 'UG-305', country_code: 'UG', text: 'Kitgum' }, { value: 'UG-316', country_code: 'UG', text: 'Koboko' }, { value: 'UG-326', country_code: 'UG', text: 'Kole' }, { value: 'UG-306', country_code: 'UG', text: 'Kotido' }, { value: 'UG-208', country_code: 'UG', text: 'Kumi' }, { value: 'UG-228', country_code: 'UG', text: 'Kween' }, { value: 'UG-123', country_code: 'UG', text: 'Kyankwanzi' }, { value: 'UG-421', country_code: 'UG', text: 'Kyegegwa' }, { value: 'UG-415', country_code: 'UG', text: 'Kyenjojo' }, { value: 'UG-307', country_code: 'UG', text: 'Lira' }, { value: 'UG-229', country_code: 'UG', text: 'Luuka' }, { value: 'UG-104', country_code: 'UG', text: 'Luwero' }, { value: 'UG-124', country_code: 'UG', text: 'Lwengo' }, { value: 'UG-116', country_code: 'UG', text: 'Lyantonde' }, { value: 'UG-221', country_code: 'UG', text: 'Manafwa' }, { value: 'UG-320', country_code: 'UG', text: 'Maracha' }, { value: 'UG-105', country_code: 'UG', text: 'Masaka' }, { value: 'UG-409', country_code: 'UG', text: 'Masindi' }, { value: 'UG-214', country_code: 'UG', text: 'Mayuge' }, { value: 'UG-209', country_code: 'UG', text: 'Mbale' }, { value: 'UG-410', country_code: 'UG', text: 'Mbarara' }, { value: 'UG-422', country_code: 'UG', text: 'Mitooma' }, { value: 'UG-114', country_code: 'UG', text: 'Mityana' }, { value: 'UG-308', country_code: 'UG', text: 'Moroto' }, { value: 'UG-309', country_code: 'UG', text: 'Moyo' }, { value: 'UG-106', country_code: 'UG', text: 'Mpigi' }, { value: 'UG-107', country_code: 'UG', text: 'Mubende' }, { value: 'UG-108', country_code: 'UG', text: 'Mukono' }, { value: 'UG-311', country_code: 'UG', text: 'Nakapiripirit' }, { value: 'UG-115', country_code: 'UG', text: 'Nakaseke' }, { value: 'UG-109', country_code: 'UG', text: 'Nakasongola' }, { value: 'UG-230', country_code: 'UG', text: 'Namayingo' }, { value: 'UG-222', country_code: 'UG', text: 'Namutumba' }, { value: 'UG-328', country_code: 'UG', text: 'Napak' }, { value: 'UG-310', country_code: 'UG', text: 'Nebbi' }, { value: 'UG-231', country_code: 'UG', text: 'Ngora' }, { value: 'UG-423', country_code: 'UG', text: 'Ntoroko' }, { value: 'UG-411', country_code: 'UG', text: 'Ntungamo' }, { value: 'UG-330', country_code: 'UG', text: 'Otuke' }, { value: 'UG-321', country_code: 'UG', text: 'Oyam' }, { value: 'UG-312', country_code: 'UG', text: 'Pader' }, { value: 'UG-210', country_code: 'UG', text: 'Pallisa' }, { value: 'UG-110', country_code: 'UG', text: 'Rakai' }, { value: 'UG-424', country_code: 'UG', text: 'Rubirizi' }, { value: 'UG-412', country_code: 'UG', text: 'Rukungiri' }, { value: 'UG-111', country_code: 'UG', text: 'Sembabule' }, { value: 'UG-232', country_code: 'UG', text: 'Serere' }, { value: 'UG-425', country_code: 'UG', text: 'Sheema' }, { value: 'UG-215', country_code: 'UG', text: 'Sironko' }, { value: 'UG-211', country_code: 'UG', text: 'Soroti' }, { value: 'UG-212', country_code: 'UG', text: 'Tororo' }, { value: 'UG-113', country_code: 'UG', text: 'Wakiso' }, { value: 'UG-313', country_code: 'UG', text: 'Yumbe' }, { value: 'UG-331', country_code: 'UG', text: 'Zombo' }, { value: 'UM-95', country_code: 'UM', text: 'Palmyra Atoll' }, { value: 'US-AL', country_code: 'US', text: 'Alabama' }, { value: 'US-AK', country_code: 'US', text: 'Alaska' }, { value: 'US-AZ', country_code: 'US', text: 'Arizona' }, { value: 'US-AR', country_code: 'US', text: 'Arkansas' }, { value: 'US-CA', country_code: 'US', text: 'California' }, { value: 'US-CO', country_code: 'US', text: 'Colorado' }, { value: 'US-CT', country_code: 'US', text: 'Connecticut' }, { value: 'US-DE', country_code: 'US', text: 'Delaware' }, { value: 'US-DC', country_code: 'US', text: 'District of Columbia' }, { value: 'US-FL', country_code: 'US', text: 'Florida' }, { value: 'US-GA', country_code: 'US', text: 'Georgia' }, { value: 'US-HI', country_code: 'US', text: 'Hawaii' }, { value: 'US-ID', country_code: 'US', text: 'Idaho' }, { value: 'US-IL', country_code: 'US', text: 'Illinois' }, { value: 'US-IN', country_code: 'US', text: 'Indiana' }, { value: 'US-IA', country_code: 'US', text: 'Iowa' }, { value: 'US-KS', country_code: 'US', text: 'Kansas' }, { value: 'US-KY', country_code: 'US', text: 'Kentucky' }, { value: 'US-LA', country_code: 'US', text: 'Louisiana' }, { value: 'US-ME', country_code: 'US', text: 'Maine' }, { value: 'US-MD', country_code: 'US', text: 'Maryland' }, { value: 'US-MA', country_code: 'US', text: 'Massachusetts' }, { value: 'US-MI', country_code: 'US', text: 'Michigan' }, { value: 'US-MN', country_code: 'US', text: 'Minnesota' }, { value: 'US-MS', country_code: 'US', text: 'Mississippi' }, { value: 'US-MO', country_code: 'US', text: 'Missouri' }, { value: 'US-MT', country_code: 'US', text: 'Montana' }, { value: 'US-NE', country_code: 'US', text: 'Nebraska' }, { value: 'US-NV', country_code: 'US', text: 'Nevada' }, { value: 'US-NH', country_code: 'US', text: 'New Hampshire' }, { value: 'US-NJ', country_code: 'US', text: 'New Jersey' }, { value: 'US-NM', country_code: 'US', text: 'New Mexico' }, { value: 'US-NY', country_code: 'US', text: 'New York' }, { value: 'US-NC', country_code: 'US', text: 'North Carolina' }, { value: 'US-ND', country_code: 'US', text: 'North Dakota' }, { value: 'US-OH', country_code: 'US', text: 'Ohio' }, { value: 'US-OK', country_code: 'US', text: 'Oklahoma' }, { value: 'US-OR', country_code: 'US', text: 'Oregon' }, { value: 'US-PA', country_code: 'US', text: 'Pennsylvania' }, { value: 'US-RI', country_code: 'US', text: 'Rhode Island' }, { value: 'US-SC', country_code: 'US', text: 'South Carolina' }, { value: 'US-SD', country_code: 'US', text: 'South Dakota' }, { value: 'US-TN', country_code: 'US', text: 'Tennessee' }, { value: 'US-TX', country_code: 'US', text: 'Texas' }, { value: 'US-UT', country_code: 'US', text: 'Utah' }, { value: 'US-VT', country_code: 'US', text: 'Vermont' }, { value: 'US-VA', country_code: 'US', text: 'Virginia' }, { value: 'US-WA', country_code: 'US', text: 'Washington' }, { value: 'US-WV', country_code: 'US', text: 'West Virginia' }, { value: 'US-WI', country_code: 'US', text: 'Wisconsin' }, { value: 'US-WY', country_code: 'US', text: 'Wyoming' }, { value: 'UY-AR', country_code: 'UY', text: 'Artigas' }, { value: 'UY-CA', country_code: 'UY', text: 'Canelones' }, { value: 'UY-CL', country_code: 'UY', text: 'Cerro Largo' }, { value: 'UY-CO', country_code: 'UY', text: 'Colonia' }, { value: 'UY-DU', country_code: 'UY', text: 'Durazno' }, { value: 'UY-FS', country_code: 'UY', text: 'Flores' }, { value: 'UY-FD', country_code: 'UY', text: 'Florida' }, { value: 'UY-LA', country_code: 'UY', text: 'Lavalleja' }, { value: 'UY-MA', country_code: 'UY', text: 'Maldonado' }, { value: 'UY-MO', country_code: 'UY', text: 'Montevideo' }, { value: 'UY-PA', country_code: 'UY', text: 'Paysandu' }, { value: 'UY-RN', country_code: 'UY', text: 'Rio Negro' }, { value: 'UY-RV', country_code: 'UY', text: 'Rivera' }, { value: 'UY-RO', country_code: 'UY', text: 'Rocha' }, { value: 'UY-SA', country_code: 'UY', text: 'Salto' }, { value: 'UY-SJ', country_code: 'UY', text: 'San Jose' }, { value: 'UY-SO', country_code: 'UY', text: 'Soriano' }, { value: 'UY-TA', country_code: 'UY', text: 'Tacuarembo' }, { value: 'UY-TT', country_code: 'UY', text: 'Treinta y Tres' }, { value: 'UZ-AN', country_code: 'UZ', text: 'Andijon' }, { value: 'UZ-BU', country_code: 'UZ', text: 'Buxoro' }, { value: 'UZ-FA', country_code: 'UZ', text: "Farg'ona" }, { value: 'UZ-JI', country_code: 'UZ', text: 'Jizzax' }, { value: 'UZ-NG', country_code: 'UZ', text: 'Namangan' }, { value: 'UZ-NW', country_code: 'UZ', text: 'Navoiy' }, { value: 'UZ-QA', country_code: 'UZ', text: 'Qashqadaryo' }, { value: 'UZ-QR', country_code: 'UZ', text: "Qoraqalpog'iston Respublikasi" }, { value: 'UZ-SA', country_code: 'UZ', text: 'Samarqand' }, { value: 'UZ-SI', country_code: 'UZ', text: 'Sirdaryo' }, { value: 'UZ-SU', country_code: 'UZ', text: 'Surxondaryo' }, { value: 'UZ-TK', country_code: 'UZ', text: 'Toshkent' }, { value: 'UZ-XO', country_code: 'UZ', text: 'Xorazm' }, { value: 'VC-01', country_code: 'VC', text: 'Charlotte' }, { value: 'VC-04', country_code: 'VC', text: 'Saint George' }, { value: 'VE-Z', country_code: 'VE', text: 'Amazonas' }, { value: 'VE-B', country_code: 'VE', text: 'Anzoategui' }, { value: 'VE-C', country_code: 'VE', text: 'Apure' }, { value: 'VE-D', country_code: 'VE', text: 'Aragua' }, { value: 'VE-E', country_code: 'VE', text: 'Barinas' }, { value: 'VE-F', country_code: 'VE', text: 'Bolivar' }, { value: 'VE-G', country_code: 'VE', text: 'Carabobo' }, { value: 'VE-H', country_code: 'VE', text: 'Cojedes' }, { value: 'VE-Y', country_code: 'VE', text: 'Delta Amacuro' }, { value: 'VE-A', country_code: 'VE', text: 'Distrito Capital' }, { value: 'VE-I', country_code: 'VE', text: 'Falcon' }, { value: 'VE-J', country_code: 'VE', text: 'Guarico' }, { value: 'VE-K', country_code: 'VE', text: 'Lara' }, { value: 'VE-L', country_code: 'VE', text: 'Merida' }, { value: 'VE-M', country_code: 'VE', text: 'Miranda' }, { value: 'VE-N', country_code: 'VE', text: 'Monagas' }, { value: 'VE-O', country_code: 'VE', text: 'Nueva Esparta' }, { value: 'VE-P', country_code: 'VE', text: 'Portuguesa' }, { value: 'VE-R', country_code: 'VE', text: 'Sucre' }, { value: 'VE-S', country_code: 'VE', text: 'Tachira' }, { value: 'VE-T', country_code: 'VE', text: 'Trujillo' }, { value: 'VE-X', country_code: 'VE', text: 'Vargas' }, { value: 'VE-U', country_code: 'VE', text: 'Yaracuy' }, { value: 'VE-V', country_code: 'VE', text: 'Zulia' }, { value: 'VN-44', country_code: 'VN', text: 'An Giang' }, { value: 'VN-54', country_code: 'VN', text: 'Bac Giang' }, { value: 'VN-53', country_code: 'VN', text: 'Bac Kan' }, { value: 'VN-55', country_code: 'VN', text: 'Bac Lieu' }, { value: 'VN-56', country_code: 'VN', text: 'Bac Ninh' }, { value: 'VN-50', country_code: 'VN', text: 'Ben Tre' }, { value: 'VN-31', country_code: 'VN', text: 'Binh Dinh' }, { value: 'VN-57', country_code: 'VN', text: 'Binh Duong' }, { value: 'VN-58', country_code: 'VN', text: 'Binh Phuoc' }, { value: 'VN-40', country_code: 'VN', text: 'Binh Thuan' }, { value: 'VN-59', country_code: 'VN', text: 'Ca Mau' }, { value: 'VN-CT', country_code: 'VN', text: 'Can Tho' }, { value: 'VN-04', country_code: 'VN', text: 'Cao Bang' }, { value: 'VN-DN', country_code: 'VN', text: 'Da Nang' }, { value: 'VN-33', country_code: 'VN', text: 'Dak Lak' }, { value: 'VN-71', country_code: 'VN', text: 'Dien Bien' }, { value: 'VN-39', country_code: 'VN', text: 'Dong Nai' }, { value: 'VN-45', country_code: 'VN', text: 'Dong Thap' }, { value: 'VN-30', country_code: 'VN', text: 'Gia Lai' }, { value: 'VN-03', country_code: 'VN', text: 'Ha Giang' }, { value: 'VN-63', country_code: 'VN', text: 'Ha Nam' }, { value: 'VN-HN', country_code: 'VN', text: 'Ha Noi' }, { value: 'VN-23', country_code: 'VN', text: 'Ha Tinh' }, { value: 'VN-61', country_code: 'VN', text: 'Hai Duong' }, { value: 'VN-HP', country_code: 'VN', text: 'Hai Phong' }, { value: 'VN-SG', country_code: 'VN', text: 'Ho Chi Minh' }, { value: 'VN-14', country_code: 'VN', text: 'Hoa Binh' }, { value: 'VN-66', country_code: 'VN', text: 'Hung Yen' }, { value: 'VN-34', country_code: 'VN', text: 'Khanh Hoa' }, { value: 'VN-47', country_code: 'VN', text: 'Kien Giang' }, { value: 'VN-01', country_code: 'VN', text: 'Lai Chau' }, { value: 'VN-35', country_code: 'VN', text: 'Lam Dong' }, { value: 'VN-09', country_code: 'VN', text: 'Lang Son' }, { value: 'VN-02', country_code: 'VN', text: 'Lao Cai' }, { value: 'VN-41', country_code: 'VN', text: 'Long An' }, { value: 'VN-67', country_code: 'VN', text: 'Nam Dinh' }, { value: 'VN-22', country_code: 'VN', text: 'Nghe An' }, { value: 'VN-18', country_code: 'VN', text: 'Ninh Binh' }, { value: 'VN-36', country_code: 'VN', text: 'Ninh Thuan' }, { value: 'VN-68', country_code: 'VN', text: 'Phu Tho' }, { value: 'VN-32', country_code: 'VN', text: 'Phu Yen' }, { value: 'VN-24', country_code: 'VN', text: 'Quang Binh' }, { value: 'VN-27', country_code: 'VN', text: 'Quang Nam' }, { value: 'VN-29', country_code: 'VN', text: 'Quang Ngai' }, { value: 'VN-13', country_code: 'VN', text: 'Quang Ninh' }, { value: 'VN-25', country_code: 'VN', text: 'Quang Tri' }, { value: 'VN-52', country_code: 'VN', text: 'Soc Trang' }, { value: 'VN-05', country_code: 'VN', text: 'Son La' }, { value: 'VN-37', country_code: 'VN', text: 'Tay Ninh' }, { value: 'VN-20', country_code: 'VN', text: 'Thai Binh' }, { value: 'VN-69', country_code: 'VN', text: 'Thai Nguyen' }, { value: 'VN-21', country_code: 'VN', text: 'Thanh Hoa' }, { value: 'VN-26', country_code: 'VN', text: 'Thua Thien-Hue' }, { value: 'VN-46', country_code: 'VN', text: 'Tien Giang' }, { value: 'VN-51', country_code: 'VN', text: 'Tra Vinh' }, { value: 'VN-07', country_code: 'VN', text: 'Tuyen Quang' }, { value: 'VN-49', country_code: 'VN', text: 'Vinh Long' }, { value: 'VN-70', country_code: 'VN', text: 'Vinh Phuc' }, { value: 'VN-06', country_code: 'VN', text: 'Yen Bai' }, { value: 'VU-MAP', country_code: 'VU', text: 'Malampa' }, { value: 'VU-SAM', country_code: 'VU', text: 'Sanma' }, { value: 'VU-SEE', country_code: 'VU', text: 'Shefa' }, { value: 'VU-TAE', country_code: 'VU', text: 'Tafea' }, { value: 'VU-TOB', country_code: 'VU', text: 'Torba' }, { value: 'WF-UV', country_code: 'WF', text: 'Uvea' }, { value: 'WS-AA', country_code: 'WS', text: "A'ana" }, { value: 'WS-AT', country_code: 'WS', text: 'Atua' }, { value: 'WS-GI', country_code: 'WS', text: 'Gagaifomauga' }, { value: 'WS-PA', country_code: 'WS', text: 'Palauli' }, { value: 'WS-TU', country_code: 'WS', text: 'Tuamasaga' }, { value: 'YE-AD', country_code: 'YE', text: "'Adan" }, { value: 'YE-AM', country_code: 'YE', text: "'Amran" }, { value: 'YE-AB', country_code: 'YE', text: 'Abyan' }, { value: 'YE-DA', country_code: 'YE', text: "Ad Dali'" }, { value: 'YE-BA', country_code: 'YE', text: "Al Bayda'" }, { value: 'YE-HU', country_code: 'YE', text: 'Al Hudaydah' }, { value: 'YE-JA', country_code: 'YE', text: 'Al Jawf' }, { value: 'YE-MR', country_code: 'YE', text: 'Al Mahrah' }, { value: 'YE-MW', country_code: 'YE', text: 'Al Mahwit' }, { value: 'YE-SA', country_code: 'YE', text: "Amanat al 'Asimah" }, { value: 'YE-DH', country_code: 'YE', text: 'Dhamar' }, { value: 'YE-HD', country_code: 'YE', text: 'Hadramawt' }, { value: 'YE-HJ', country_code: 'YE', text: 'Hajjah' }, { value: 'YE-IB', country_code: 'YE', text: 'Ibb' }, { value: 'YE-LA', country_code: 'YE', text: 'Lahij' }, { value: 'YE-MA', country_code: 'YE', text: "Ma'rib" }, { value: 'YE-RA', country_code: 'YE', text: 'Raymah' }, { value: 'YE-SD', country_code: 'YE', text: "Sa'dah" }, { value: 'YE-SN', country_code: 'YE', text: "San'a'" }, { value: 'YE-SH', country_code: 'YE', text: 'Shabwah' }, { value: 'YE-TA', country_code: 'YE', text: "Ta'izz" }, { value: 'ZA-EC', country_code: 'ZA', text: 'Eastern Cape' }, { value: 'ZA-FS', country_code: 'ZA', text: 'Free State' }, { value: 'ZA-GT', country_code: 'ZA', text: 'Gauteng' }, { value: 'ZA-NL', country_code: 'ZA', text: 'Kwazulu-Natal' }, { value: 'ZA-LP', country_code: 'ZA', text: 'Limpopo' }, { value: 'ZA-MP', country_code: 'ZA', text: 'Mpumalanga' }, { value: 'ZA-NW', country_code: 'ZA', text: 'North-West' }, { value: 'ZA-NC', country_code: 'ZA', text: 'Northern Cape' }, { value: 'ZA-WC', country_code: 'ZA', text: 'Western Cape' }, { value: 'ZM-02', country_code: 'ZM', text: 'Central' }, { value: 'ZM-08', country_code: 'ZM', text: 'Copperbelt' }, { value: 'ZM-03', country_code: 'ZM', text: 'Eastern' }, { value: 'ZM-04', country_code: 'ZM', text: 'Luapula' }, { value: 'ZM-09', country_code: 'ZM', text: 'Lusaka' }, { value: 'ZM-06', country_code: 'ZM', text: 'North-Western' }, { value: 'ZM-05', country_code: 'ZM', text: 'Northern' }, { value: 'ZM-07', country_code: 'ZM', text: 'Southern' }, { value: 'ZM-01', country_code: 'ZM', text: 'Western' }, { value: 'ZW-BU', country_code: 'ZW', text: 'Bulawayo' }, { value: 'ZW-HA', country_code: 'ZW', text: 'Harare' }, { value: 'ZW-MA', country_code: 'ZW', text: 'Manicaland' }, { value: 'ZW-MC', country_code: 'ZW', text: 'Mashonaland Central' }, { value: 'ZW-ME', country_code: 'ZW', text: 'Mashonaland East' }, { value: 'ZW-MW', country_code: 'ZW', text: 'Mashonaland West' }, { value: 'ZW-MV', country_code: 'ZW', text: 'Masvingo' }, { value: 'ZW-MN', country_code: 'ZW', text: 'Matabeleland North' }, { value: 'ZW-MS', country_code: 'ZW', text: 'Matabeleland South' }, { value: 'ZW-MI', country_code: 'ZW', text: 'Midlands' } ].filter((p) => p.country_code === country); const childrenWithProps = React.Children.map(children, (child) => React.cloneElement(child, { options: options.filter((o) => o.country_code === country), ...props }) ); return <div>{childrenWithProps}</div>; } ProvinceOptions.propTypes = { children: PropTypes.node.isRequired, country: PropTypes.string.isRequired }; export { ProvinceOptions }; ================================================ FILE: packages/evershop/src/components/common/locale/TimezoneOption.jsx ================================================ import PropTypes from 'prop-types'; import React from 'react'; function TimezoneOptions(props) { const { timezones, children } = props; const options = [ { value: 'Australia/Darwin', text: 'AUS Central Standard Time (Australia/Darwin)' }, { value: 'Australia/Melbourne', text: 'AUS Eastern Standard Time (Australia/Melbourne)' }, { value: 'Australia/Sydney', text: 'AUS Eastern Standard Time (Australia/Sydney)' }, { value: 'Asia/Kabul', text: 'Afghanistan Standard Time (Asia/Kabul)' }, { value: 'America/Anchorage', text: 'Alaskan Standard Time (America/Anchorage)' }, { value: 'America/Juneau', text: 'Alaskan Standard Time (America/Juneau)' }, { value: 'America/Nome', text: 'Alaskan Standard Time (America/Nome)' }, { value: 'America/Sitka', text: 'Alaskan Standard Time (America/Sitka)' }, { value: 'America/Yakutat', text: 'Alaskan Standard Time (America/Yakutat)' }, { value: 'Asia/Aden', text: 'Arab Standard Time (Asia/Aden)' }, { value: 'Asia/Bahrain', text: 'Arab Standard Time (Asia/Bahrain)' }, { value: 'Asia/Kuwait', text: 'Arab Standard Time (Asia/Kuwait)' }, { value: 'Asia/Qatar', text: 'Arab Standard Time (Asia/Qatar)' }, { value: 'Asia/Riyadh', text: 'Arab Standard Time (Asia/Riyadh)' }, { value: 'Asia/Dubai', text: 'Arabian Standard Time (Asia/Dubai)' }, { value: 'Asia/Muscat', text: 'Arabian Standard Time (Asia/Muscat)' }, { value: 'Etc/GMT-4', text: 'Arabian Standard Time (Etc/GMT-4)' }, { value: 'Asia/Baghdad', text: 'Arabic Standard Time (Asia/Baghdad)' }, { value: 'America/Argentina/La_Rioja', text: 'Argentina Standard Time (America/Argentina/La_Rioja)' }, { value: 'America/Argentina/Rio_Gallegos', text: 'Argentina Standard Time (America/Argentina/Rio_Gallegos)' }, { value: 'America/Argentina/Salta', text: 'Argentina Standard Time (America/Argentina/Salta)' }, { value: 'America/Argentina/San_Juan', text: 'Argentina Standard Time (America/Argentina/San_Juan)' }, { value: 'America/Argentina/San_Luis', text: 'Argentina Standard Time (America/Argentina/San_Luis)' }, { value: 'America/Argentina/Tucuman', text: 'Argentina Standard Time (America/Argentina/Tucuman)' }, { value: 'America/Argentina/Ushuaia', text: 'Argentina Standard Time (America/Argentina/Ushuaia)' }, { value: 'America/Buenos_Aires', text: 'Argentina Standard Time (America/Buenos_Aires)' }, { value: 'America/Catamarca', text: 'Argentina Standard Time (America/Catamarca)' }, { value: 'America/Cordoba', text: 'Argentina Standard Time (America/Cordoba)' }, { value: 'America/Jujuy', text: 'Argentina Standard Time (America/Jujuy)' }, { value: 'America/Mendoza', text: 'Argentina Standard Time (America/Mendoza)' }, { value: 'America/Glace_Bay', text: 'Atlantic Standard Time (America/Glace_Bay)' }, { value: 'America/Goose_Bay', text: 'Atlantic Standard Time (America/Goose_Bay)' }, { value: 'America/Halifax', text: 'Atlantic Standard Time (America/Halifax)' }, { value: 'America/Moncton', text: 'Atlantic Standard Time (America/Moncton)' }, { value: 'America/Thule', text: 'Atlantic Standard Time (America/Thule)' }, { value: 'Atlantic/Bermuda', text: 'Atlantic Standard Time (Atlantic/Bermuda)' }, { value: 'Asia/Baku', text: 'Azerbaijan Standard Time (Asia/Baku)' }, { value: 'America/Scoresbysund', text: 'Azores Standard Time (America/Scoresbysund)' }, { value: 'Atlantic/Azores', text: 'Azores Standard Time (Atlantic/Azores)' }, { value: 'America/Bahia', text: 'Bahia Standard Time (America/Bahia)' }, { value: 'Asia/Dhaka', text: 'Bangladesh Standard Time (Asia/Dhaka)' }, { value: 'Asia/Thimphu', text: 'Bangladesh Standard Time (Asia/Thimphu)' }, { value: 'America/Regina', text: 'Canada Central Standard Time (America/Regina)' }, { value: 'America/Swift_Current', text: 'Canada Central Standard Time (America/Swift_Current)' }, { value: 'Atlantic/Cape_Verde', text: 'Cape Verde Standard Time (Atlantic/Cape_Verde)' }, { value: 'Etc/GMT+1', text: 'Cape Verde Standard Time (Etc/GMT+1)' }, { value: 'Asia/Yerevan', text: 'Caucasus Standard Time (Asia/Yerevan)' }, { value: 'Australia/Adelaide', text: 'Cen. Australia Standard Time (Australia/Adelaide)' }, { value: 'Australia/Broken_Hill', text: 'Cen. Australia Standard Time (Australia/Broken_Hill)' }, { value: 'America/Belize', text: 'Central America Standard Time (America/Belize)' }, { value: 'America/Costa_Rica', text: 'Central America Standard Time (America/Costa_Rica)' }, { value: 'America/El_Salvador', text: 'Central America Standard Time (America/El_Salvador)' }, { value: 'America/Guatemala', text: 'Central America Standard Time (America/Guatemala)' }, { value: 'America/Managua', text: 'Central America Standard Time (America/Managua)' }, { value: 'America/Tegucigalpa', text: 'Central America Standard Time (America/Tegucigalpa)' }, { value: 'Etc/GMT+6', text: 'Central America Standard Time (Etc/GMT+6)' }, { value: 'Pacific/Galapagos', text: 'Central America Standard Time (Pacific/Galapagos)' }, { value: 'Antarctica/Vostok', text: 'Central Asia Standard Time (Antarctica/Vostok)' }, { value: 'Asia/Almaty', text: 'Central Asia Standard Time (Asia/Almaty)' }, { value: 'Asia/Bishkek', text: 'Central Asia Standard Time (Asia/Bishkek)' }, { value: 'Asia/Qyzylorda', text: 'Central Asia Standard Time (Asia/Qyzylorda)' }, { value: 'Etc/GMT-6', text: 'Central Asia Standard Time (Etc/GMT-6)' }, { value: 'Indian/Chagos', text: 'Central Asia Standard Time (Indian/Chagos)' }, { value: 'America/Campo_Grande', text: 'Central Brazilian Standard Time (America/Campo_Grande)' }, { value: 'America/Cuiaba', text: 'Central Brazilian Standard Time (America/Cuiaba)' }, { value: 'Europe/Belgrade', text: 'Central Europe Standard Time (Europe/Belgrade)' }, { value: 'Europe/Bratislava', text: 'Central Europe Standard Time (Europe/Bratislava)' }, { value: 'Europe/Budapest', text: 'Central Europe Standard Time (Europe/Budapest)' }, { value: 'Europe/Ljubljana', text: 'Central Europe Standard Time (Europe/Ljubljana)' }, { value: 'Europe/Podgorica', text: 'Central Europe Standard Time (Europe/Podgorica)' }, { value: 'Europe/Prague', text: 'Central Europe Standard Time (Europe/Prague)' }, { value: 'Europe/Tirane', text: 'Central Europe Standard Time (Europe/Tirane)' }, { value: 'Europe/Sarajevo', text: 'Central European Standard Time (Europe/Sarajevo)' }, { value: 'Europe/Skopje', text: 'Central European Standard Time (Europe/Skopje)' }, { value: 'Europe/Warsaw', text: 'Central European Standard Time (Europe/Warsaw)' }, { value: 'Europe/Zagreb', text: 'Central European Standard Time (Europe/Zagreb)' }, { value: 'Antarctica/Macquarie', text: 'Central Pacific Standard Time (Antarctica/Macquarie)' }, { value: 'Etc/GMT-11', text: 'Central Pacific Standard Time (Etc/GMT-11)' }, { value: 'Pacific/Efate', text: 'Central Pacific Standard Time (Pacific/Efate)' }, { value: 'Pacific/Guadalcanal', text: 'Central Pacific Standard Time (Pacific/Guadalcanal)' }, { value: 'Pacific/Kosrae', text: 'Central Pacific Standard Time (Pacific/Kosrae)' }, { value: 'Pacific/Noumea', text: 'Central Pacific Standard Time (Pacific/Noumea)' }, { value: 'Pacific/Ponape', text: 'Central Pacific Standard Time (Pacific/Ponape)' }, { value: 'America/Chicago', text: 'Central Standard Time (America/Chicago)' }, { value: 'America/Indiana/Knox', text: 'Central Standard Time (America/Indiana/Knox)' }, { value: 'America/Indiana/Tell_City', text: 'Central Standard Time (America/Indiana/Tell_City)' }, { value: 'America/Matamoros', text: 'Central Standard Time (America/Matamoros)' }, { value: 'America/Menominee', text: 'Central Standard Time (America/Menominee)' }, { value: 'America/North_Dakota/Beulah', text: 'Central Standard Time (America/North_Dakota/Beulah)' }, { value: 'America/North_Dakota/Center', text: 'Central Standard Time (America/North_Dakota/Center)' }, { value: 'America/North_Dakota/New_Salem', text: 'Central Standard Time (America/North_Dakota/New_Salem)' }, { value: 'America/Rainy_River', text: 'Central Standard Time (America/Rainy_River)' }, { value: 'America/Rankin_Inlet', text: 'Central Standard Time (America/Rankin_Inlet)' }, { value: 'America/Resolute', text: 'Central Standard Time (America/Resolute)' }, { value: 'America/Winnipeg', text: 'Central Standard Time (America/Winnipeg)' }, { value: 'CST6CDT', text: 'Central Standard Time (CST6CDT)' }, { value: 'America/Bahia_Banderas', text: 'Central Standard Time (Mexico) (America/Bahia_Banderas)' }, { value: 'America/Cancun', text: 'Central Standard Time (Mexico) (America/Cancun)' }, { value: 'America/Merida', text: 'Central Standard Time (Mexico) (America/Merida)' }, { value: 'America/Mexico_City', text: 'Central Standard Time (Mexico) (America/Mexico_City)' }, { value: 'America/Monterrey', text: 'Central Standard Time (Mexico) (America/Monterrey)' }, { value: 'Asia/Chongqing', text: 'China Standard Time (Asia/Chongqing)' }, { value: 'Asia/Harbin', text: 'China Standard Time (Asia/Harbin)' }, { value: 'Asia/Hong_Kong', text: 'China Standard Time (Asia/Hong_Kong)' }, { value: 'Asia/Kashgar', text: 'China Standard Time (Asia/Kashgar)' }, { value: 'Asia/Macau', text: 'China Standard Time (Asia/Macau)' }, { value: 'Asia/Shanghai', text: 'China Standard Time (Asia/Shanghai)' }, { value: 'Asia/Urumqi', text: 'China Standard Time (Asia/Urumqi)' }, { value: 'Etc/GMT+12', text: 'Dateline Standard Time (Etc/GMT+12)' }, { value: 'Africa/Addis_Ababa', text: 'E. Africa Standard Time (Africa/Addis_Ababa)' }, { value: 'Africa/Asmera', text: 'E. Africa Standard Time (Africa/Asmera)' }, { value: 'Africa/Dar_es_Salaam', text: 'E. Africa Standard Time (Africa/Dar_es_Salaam)' }, { value: 'Africa/Djibouti', text: 'E. Africa Standard Time (Africa/Djibouti)' }, { value: 'Africa/Juba', text: 'E. Africa Standard Time (Africa/Juba)' }, { value: 'Africa/Kampala', text: 'E. Africa Standard Time (Africa/Kampala)' }, { value: 'Africa/Khartoum', text: 'E. Africa Standard Time (Africa/Khartoum)' }, { value: 'Africa/Mogadishu', text: 'E. Africa Standard Time (Africa/Mogadishu)' }, { value: 'Africa/Nairobi', text: 'E. Africa Standard Time (Africa/Nairobi)' }, { value: 'Antarctica/Syowa', text: 'E. Africa Standard Time (Antarctica/Syowa)' }, { value: 'Etc/GMT-3', text: 'E. Africa Standard Time (Etc/GMT-3)' }, { value: 'Indian/Antananarivo', text: 'E. Africa Standard Time (Indian/Antananarivo)' }, { value: 'Indian/Comoro', text: 'E. Africa Standard Time (Indian/Comoro)' }, { value: 'Indian/Mayotte', text: 'E. Africa Standard Time (Indian/Mayotte)' }, { value: 'Australia/Brisbane', text: 'E. Australia Standard Time (Australia/Brisbane)' }, { value: 'Australia/Lindeman', text: 'E. Australia Standard Time (Australia/Lindeman)' }, { value: 'America/Sao_Paulo', text: 'E. South America Standard Time (America/Sao_Paulo)' }, { value: 'America/Detroit', text: 'Eastern Standard Time (America/Detroit)' }, { value: 'America/Grand_Turk', text: 'Eastern Standard Time (America/Grand_Turk)' }, { value: 'America/Havana', text: 'Eastern Standard Time (America/Havana)' }, { value: 'America/Indiana/Petersburg', text: 'Eastern Standard Time (America/Indiana/Petersburg)' }, { value: 'America/Indiana/Vincennes', text: 'Eastern Standard Time (America/Indiana/Vincennes)' }, { value: 'America/Indiana/Winamac', text: 'Eastern Standard Time (America/Indiana/Winamac)' }, { value: 'America/Iqaluit', text: 'Eastern Standard Time (America/Iqaluit)' }, { value: 'America/Kentucky/Monticello', text: 'Eastern Standard Time (America/Kentucky/Monticello)' }, { value: 'America/Louisville', text: 'Eastern Standard Time (America/Louisville)' }, { value: 'America/Montreal', text: 'Eastern Standard Time (America/Montreal)' }, { value: 'America/Nassau', text: 'Eastern Standard Time (America/Nassau)' }, { value: 'America/New_York', text: 'Eastern Standard Time (America/New_York)' }, { value: 'America/Nipigon', text: 'Eastern Standard Time (America/Nipigon)' }, { value: 'America/Pangnirtung', text: 'Eastern Standard Time (America/Pangnirtung)' }, { value: 'America/Port-au-Prince', text: 'Eastern Standard Time (America/Port-au-Prince)' }, { value: 'America/Thunder_Bay', text: 'Eastern Standard Time (America/Thunder_Bay)' }, { value: 'America/Toronto', text: 'Eastern Standard Time (America/Toronto)' }, { value: 'EST5EDT', text: 'Eastern Standard Time (EST5EDT)' }, { value: 'Africa/Cairo', text: 'Egypt Standard Time (Africa/Cairo)' }, { value: 'Asia/Yekaterinburg', text: 'Ekaterinburg Standard Time (Asia/Yekaterinburg)' }, { value: 'Europe/Helsinki', text: 'FLE Standard Time (Europe/Helsinki)' }, { value: 'Europe/Kiev', text: 'FLE Standard Time (Europe/Kiev)' }, { value: 'Europe/Mariehamn', text: 'FLE Standard Time (Europe/Mariehamn)' }, { value: 'Europe/Riga', text: 'FLE Standard Time (Europe/Riga)' }, { value: 'Europe/Simferopol', text: 'FLE Standard Time (Europe/Simferopol)' }, { value: 'Europe/Sofia', text: 'FLE Standard Time (Europe/Sofia)' }, { value: 'Europe/Tallinn', text: 'FLE Standard Time (Europe/Tallinn)' }, { value: 'Europe/Uzhgorod', text: 'FLE Standard Time (Europe/Uzhgorod)' }, { value: 'Europe/Vilnius', text: 'FLE Standard Time (Europe/Vilnius)' }, { value: 'Europe/Zaporozhye', text: 'FLE Standard Time (Europe/Zaporozhye)' }, { value: 'Pacific/Fiji', text: 'Fiji Standard Time (Pacific/Fiji)' }, { value: 'Atlantic/Canary', text: 'GMT Standard Time (Atlantic/Canary)' }, { value: 'Atlantic/Faeroe', text: 'GMT Standard Time (Atlantic/Faeroe)' }, { value: 'Atlantic/Madeira', text: 'GMT Standard Time (Atlantic/Madeira)' }, { value: 'Europe/Dublin', text: 'GMT Standard Time (Europe/Dublin)' }, { value: 'Europe/Guernsey', text: 'GMT Standard Time (Europe/Guernsey)' }, { value: 'Europe/Isle_of_Man', text: 'GMT Standard Time (Europe/Isle_of_Man)' }, { value: 'Europe/Jersey', text: 'GMT Standard Time (Europe/Jersey)' }, { value: 'Europe/Lisbon', text: 'GMT Standard Time (Europe/Lisbon)' }, { value: 'Europe/London', text: 'GMT Standard Time (Europe/London)' }, { value: 'Asia/Nicosia', text: 'GTB Standard Time (Asia/Nicosia)' }, { value: 'Europe/Athens', text: 'GTB Standard Time (Europe/Athens)' }, { value: 'Europe/Bucharest', text: 'GTB Standard Time (Europe/Bucharest)' }, { value: 'Europe/Chisinau', text: 'GTB Standard Time (Europe/Chisinau)' }, { value: 'Asia/Tbilisi', text: 'Georgian Standard Time (Asia/Tbilisi)' }, { value: 'America/Godthab', text: 'Greenland Standard Time (America/Godthab)' }, { value: 'Africa/Abidjan', text: 'Greenwich Standard Time (Africa/Abidjan)' }, { value: 'Africa/Accra', text: 'Greenwich Standard Time (Africa/Accra)' }, { value: 'Africa/Bamako', text: 'Greenwich Standard Time (Africa/Bamako)' }, { value: 'Africa/Banjul', text: 'Greenwich Standard Time (Africa/Banjul)' }, { value: 'Africa/Bissau', text: 'Greenwich Standard Time (Africa/Bissau)' }, { value: 'Africa/Conakry', text: 'Greenwich Standard Time (Africa/Conakry)' }, { value: 'Africa/Dakar', text: 'Greenwich Standard Time (Africa/Dakar)' }, { value: 'Africa/Freetown', text: 'Greenwich Standard Time (Africa/Freetown)' }, { value: 'Africa/Lome', text: 'Greenwich Standard Time (Africa/Lome)' }, { value: 'Africa/Monrovia', text: 'Greenwich Standard Time (Africa/Monrovia)' }, { value: 'Africa/Nouakchott', text: 'Greenwich Standard Time (Africa/Nouakchott)' }, { value: 'Africa/Ouagadougou', text: 'Greenwich Standard Time (Africa/Ouagadougou)' }, { value: 'Africa/Sao_Tome', text: 'Greenwich Standard Time (Africa/Sao_Tome)' }, { value: 'Atlantic/Reykjavik', text: 'Greenwich Standard Time (Atlantic/Reykjavik)' }, { value: 'Atlantic/St_Helena', text: 'Greenwich Standard Time (Atlantic/St_Helena)' }, { value: 'Etc/GMT+10', text: 'Hawaiian Standard Time (Etc/GMT+10)' }, { value: 'Pacific/Honolulu', text: 'Hawaiian Standard Time (Pacific/Honolulu)' }, { value: 'Pacific/Johnston', text: 'Hawaiian Standard Time (Pacific/Johnston)' }, { value: 'Pacific/Rarotonga', text: 'Hawaiian Standard Time (Pacific/Rarotonga)' }, { value: 'Pacific/Tahiti', text: 'Hawaiian Standard Time (Pacific/Tahiti)' }, { value: 'Asia/Calcutta', text: 'India Standard Time (Asia/Calcutta)' }, { value: 'Asia/Tehran', text: 'Iran Standard Time (Asia/Tehran)' }, { value: 'Asia/Jerusalem', text: 'Israel Standard Time (Asia/Jerusalem)' }, { value: 'Asia/Amman', text: 'Jordan Standard Time (Asia/Amman)' }, { value: 'Europe/Kaliningrad', text: 'Kaliningrad Standard Time (Europe/Kaliningrad)' }, { value: 'Europe/Minsk', text: 'Kaliningrad Standard Time (Europe/Minsk)' }, { value: 'Asia/Pyongyang', text: 'Korea Standard Time (Asia/Pyongyang)' }, { value: 'Asia/Seoul', text: 'Korea Standard Time (Asia/Seoul)' }, { value: 'Africa/Tripoli', text: 'Libya Standard Time (Africa/Tripoli)' }, { value: 'Asia/Anadyr', text: 'Magadan Standard Time (Asia/Anadyr)' }, { value: 'Asia/Kamchatka', text: 'Magadan Standard Time (Asia/Kamchatka)' }, { value: 'Asia/Magadan', text: 'Magadan Standard Time (Asia/Magadan)' }, { value: 'Indian/Mahe', text: 'Mauritius Standard Time (Indian/Mahe)' }, { value: 'Indian/Mauritius', text: 'Mauritius Standard Time (Indian/Mauritius)' }, { value: 'Indian/Reunion', text: 'Mauritius Standard Time (Indian/Reunion)' }, { value: 'Asia/Beirut', text: 'Middle East Standard Time (Asia/Beirut)' }, { value: 'America/Montevideo', text: 'Montevideo Standard Time (America/Montevideo)' }, { value: 'Africa/Casablanca', text: 'Morocco Standard Time (Africa/Casablanca)' }, { value: 'Africa/El_Aaiun', text: 'Morocco Standard Time (Africa/El_Aaiun)' }, { value: 'America/Boise', text: 'Mountain Standard Time (America/Boise)' }, { value: 'America/Cambridge_Bay', text: 'Mountain Standard Time (America/Cambridge_Bay)' }, { value: 'America/Denver', text: 'Mountain Standard Time (America/Denver)' }, { value: 'America/Edmonton', text: 'Mountain Standard Time (America/Edmonton)' }, { value: 'America/Inuvik', text: 'Mountain Standard Time (America/Inuvik)' }, { value: 'America/Ojinaga', text: 'Mountain Standard Time (America/Ojinaga)' }, { value: 'America/Shiprock', text: 'Mountain Standard Time (America/Shiprock)' }, { value: 'America/Yellowknife', text: 'Mountain Standard Time (America/Yellowknife)' }, { value: 'MST7MDT', text: 'Mountain Standard Time (MST7MDT)' }, { value: 'America/Chihuahua', text: 'Mountain Standard Time (Mexico) (America/Chihuahua)' }, { value: 'America/Mazatlan', text: 'Mountain Standard Time (Mexico) (America/Mazatlan)' }, { value: 'Asia/Rangoon', text: 'Myanmar Standard Time (Asia/Rangoon)' }, { value: 'Indian/Cocos', text: 'Myanmar Standard Time (Indian/Cocos)' }, { value: 'Asia/Novokuznetsk', text: 'N. Central Asia Standard Time (Asia/Novokuznetsk)' }, { value: 'Asia/Novosibirsk', text: 'N. Central Asia Standard Time (Asia/Novosibirsk)' }, { value: 'Asia/Omsk', text: 'N. Central Asia Standard Time (Asia/Omsk)' }, { value: 'Africa/Windhoek', text: 'Namibia Standard Time (Africa/Windhoek)' }, { value: 'Asia/Katmandu', text: 'Nepal Standard Time (Asia/Katmandu)' }, { value: 'Antarctica/McMurdo', text: 'New Zealand Standard Time (Antarctica/McMurdo)' }, { value: 'Antarctica/South_Pole', text: 'New Zealand Standard Time (Antarctica/South_Pole)' }, { value: 'Pacific/Auckland', text: 'New Zealand Standard Time (Pacific/Auckland)' }, { value: 'America/St_Johns', text: 'Newfoundland Standard Time (America/St_Johns)' }, { value: 'Asia/Irkutsk', text: 'North Asia East Standard Time (Asia/Irkutsk)' }, { value: 'Asia/Krasnoyarsk', text: 'North Asia Standard Time (Asia/Krasnoyarsk)' }, { value: 'America/Santiago', text: 'Pacific SA Standard Time (America/Santiago)' }, { value: 'Antarctica/Palmer', text: 'Pacific SA Standard Time (Antarctica/Palmer)' }, { value: 'America/Dawson', text: 'Pacific Standard Time (America/Dawson)' }, { value: 'America/Los_Angeles', text: 'Pacific Standard Time (America/Los_Angeles)' }, { value: 'America/Tijuana', text: 'Pacific Standard Time (America/Tijuana)' }, { value: 'America/Vancouver', text: 'Pacific Standard Time (America/Vancouver)' }, { value: 'America/Whitehorse', text: 'Pacific Standard Time (America/Whitehorse)' }, { value: 'America/Santa_Isabel', text: 'Pacific Standard Time (Mexico) (America/Santa_Isabel)' }, { value: 'PST8PDT', text: 'Pacific Standard Time (PST8PDT)' }, { value: 'Asia/Karachi', text: 'Pakistan Standard Time (Asia/Karachi)' }, { value: 'America/Asuncion', text: 'Paraguay Standard Time (America/Asuncion)' }, { value: 'Africa/Ceuta', text: 'Romance Standard Time (Africa/Ceuta)' }, { value: 'Europe/Brussels', text: 'Romance Standard Time (Europe/Brussels)' }, { value: 'Europe/Copenhagen', text: 'Romance Standard Time (Europe/Copenhagen)' }, { value: 'Europe/Madrid', text: 'Romance Standard Time (Europe/Madrid)' }, { value: 'Europe/Paris', text: 'Romance Standard Time (Europe/Paris)' }, { value: 'Europe/Moscow', text: 'Russian Standard Time (Europe/Moscow)' }, { value: 'Europe/Samara', text: 'Russian Standard Time (Europe/Samara)' }, { value: 'Europe/Volgograd', text: 'Russian Standard Time (Europe/Volgograd)' }, { value: 'America/Araguaina', text: 'SA Eastern Standard Time (America/Araguaina)' }, { value: 'America/Belem', text: 'SA Eastern Standard Time (America/Belem)' }, { value: 'America/Cayenne', text: 'SA Eastern Standard Time (America/Cayenne)' }, { value: 'America/Fortaleza', text: 'SA Eastern Standard Time (America/Fortaleza)' }, { value: 'America/Maceio', text: 'SA Eastern Standard Time (America/Maceio)' }, { value: 'America/Paramaribo', text: 'SA Eastern Standard Time (America/Paramaribo)' }, { value: 'America/Recife', text: 'SA Eastern Standard Time (America/Recife)' }, { value: 'America/Santarem', text: 'SA Eastern Standard Time (America/Santarem)' }, { value: 'Antarctica/Rothera', text: 'SA Eastern Standard Time (Antarctica/Rothera)' }, { value: 'Atlantic/Stanley', text: 'SA Eastern Standard Time (Atlantic/Stanley)' }, { value: 'Etc/GMT+3', text: 'SA Eastern Standard Time (Etc/GMT+3)' }, { value: 'America/Bogota', text: 'SA Pacific Standard Time (America/Bogota)' }, { value: 'America/Cayman', text: 'SA Pacific Standard Time (America/Cayman)' }, { value: 'America/Coral_Harbour', text: 'SA Pacific Standard Time (America/Coral_Harbour)' }, { value: 'America/Eirunepe', text: 'SA Pacific Standard Time (America/Eirunepe)' }, { value: 'America/Guayaquil', text: 'SA Pacific Standard Time (America/Guayaquil)' }, { value: 'America/Jamaica', text: 'SA Pacific Standard Time (America/Jamaica)' }, { value: 'America/Lima', text: 'SA Pacific Standard Time (America/Lima)' }, { value: 'America/Panama', text: 'SA Pacific Standard Time (America/Panama)' }, { value: 'America/Rio_Branco', text: 'SA Pacific Standard Time (America/Rio_Branco)' }, { value: 'Etc/GMT+5', text: 'SA Pacific Standard Time (Etc/GMT+5)' }, { value: 'America/Anguilla', text: 'SA Western Standard Time (America/Anguilla)' }, { value: 'America/Antigua', text: 'SA Western Standard Time (America/Antigua)' }, { value: 'America/Aruba', text: 'SA Western Standard Time (America/Aruba)' }, { value: 'America/Barbados', text: 'SA Western Standard Time (America/Barbados)' }, { value: 'America/Blanc-Sablon', text: 'SA Western Standard Time (America/Blanc-Sablon)' }, { value: 'America/Boa_Vista', text: 'SA Western Standard Time (America/Boa_Vista)' }, { value: 'America/Curacao', text: 'SA Western Standard Time (America/Curacao)' }, { value: 'America/Dominica', text: 'SA Western Standard Time (America/Dominica)' }, { value: 'America/Grenada', text: 'SA Western Standard Time (America/Grenada)' }, { value: 'America/Guadeloupe', text: 'SA Western Standard Time (America/Guadeloupe)' }, { value: 'America/Guyana', text: 'SA Western Standard Time (America/Guyana)' }, { value: 'America/Kralendijk', text: 'SA Western Standard Time (America/Kralendijk)' }, { value: 'America/La_Paz', text: 'SA Western Standard Time (America/La_Paz)' }, { value: 'America/Lower_Princes', text: 'SA Western Standard Time (America/Lower_Princes)' }, { value: 'America/Manaus', text: 'SA Western Standard Time (America/Manaus)' }, { value: 'America/Marigot', text: 'SA Western Standard Time (America/Marigot)' }, { value: 'America/Martinique', text: 'SA Western Standard Time (America/Martinique)' }, { value: 'America/Montserrat', text: 'SA Western Standard Time (America/Montserrat)' }, { value: 'America/Port_of_Spain', text: 'SA Western Standard Time (America/Port_of_Spain)' }, { value: 'America/Porto_Velho', text: 'SA Western Standard Time (America/Porto_Velho)' }, { value: 'America/Puerto_Rico', text: 'SA Western Standard Time (America/Puerto_Rico)' }, { value: 'America/Santo_Domingo', text: 'SA Western Standard Time (America/Santo_Domingo)' }, { value: 'America/St_Barthelemy', text: 'SA Western Standard Time (America/St_Barthelemy)' }, { value: 'America/St_Kitts', text: 'SA Western Standard Time (America/St_Kitts)' }, { value: 'America/St_Lucia', text: 'SA Western Standard Time (America/St_Lucia)' }, { value: 'America/St_Thomas', text: 'SA Western Standard Time (America/St_Thomas)' }, { value: 'America/St_Vincent', text: 'SA Western Standard Time (America/St_Vincent)' }, { value: 'America/Tortola', text: 'SA Western Standard Time (America/Tortola)' }, { value: 'Etc/GMT+4', text: 'SA Western Standard Time (Etc/GMT+4)' }, { value: 'Antarctica/Davis', text: 'SE Asia Standard Time (Antarctica/Davis)' }, { value: 'Asia/Bangkok', text: 'SE Asia Standard Time (Asia/Bangkok)' }, { value: 'Asia/Hovd', text: 'SE Asia Standard Time (Asia/Hovd)' }, { value: 'Asia/Jakarta', text: 'SE Asia Standard Time (Asia/Jakarta)' }, { value: 'Asia/Phnom_Penh', text: 'SE Asia Standard Time (Asia/Phnom_Penh)' }, { value: 'Asia/Pontianak', text: 'SE Asia Standard Time (Asia/Pontianak)' }, { value: 'Asia/Saigon', text: 'SE Asia Standard Time (Asia/Saigon)' }, { value: 'Asia/Vientiane', text: 'SE Asia Standard Time (Asia/Vientiane)' }, { value: 'Etc/GMT-7', text: 'SE Asia Standard Time (Etc/GMT-7)' }, { value: 'Indian/Christmas', text: 'SE Asia Standard Time (Indian/Christmas)' }, { value: 'Pacific/Apia', text: 'Samoa Standard Time (Pacific/Apia)' }, { value: 'Asia/Brunei', text: 'Singapore Standard Time (Asia/Brunei)' }, { value: 'Asia/Kuala_Lumpur', text: 'Singapore Standard Time (Asia/Kuala_Lumpur)' }, { value: 'Asia/Kuching', text: 'Singapore Standard Time (Asia/Kuching)' }, { value: 'Asia/Makassar', text: 'Singapore Standard Time (Asia/Makassar)' }, { value: 'Asia/Manila', text: 'Singapore Standard Time (Asia/Manila)' }, { value: 'Asia/Singapore', text: 'Singapore Standard Time (Asia/Singapore)' }, { value: 'Etc/GMT-8', text: 'Singapore Standard Time (Etc/GMT-8)' }, { value: 'Africa/Blantyre', text: 'South Africa Standard Time (Africa/Blantyre)' }, { value: 'Africa/Bujumbura', text: 'South Africa Standard Time (Africa/Bujumbura)' }, { value: 'Africa/Gaborone', text: 'South Africa Standard Time (Africa/Gaborone)' }, { value: 'Africa/Harare', text: 'South Africa Standard Time (Africa/Harare)' }, { value: 'Africa/Johannesburg', text: 'South Africa Standard Time (Africa/Johannesburg)' }, { value: 'Africa/Kigali', text: 'South Africa Standard Time (Africa/Kigali)' }, { value: 'Africa/Lubumbashi', text: 'South Africa Standard Time (Africa/Lubumbashi)' }, { value: 'Africa/Lusaka', text: 'South Africa Standard Time (Africa/Lusaka)' }, { value: 'Africa/Maputo', text: 'South Africa Standard Time (Africa/Maputo)' }, { value: 'Africa/Maseru', text: 'South Africa Standard Time (Africa/Maseru)' }, { value: 'Africa/Mbabane', text: 'South Africa Standard Time (Africa/Mbabane)' }, { value: 'Etc/GMT-2', text: 'South Africa Standard Time (Etc/GMT-2)' }, { value: 'Asia/Colombo', text: 'Sri Lanka Standard Time (Asia/Colombo)' }, { value: 'Asia/Damascus', text: 'Syria Standard Time (Asia/Damascus)' }, { value: 'Asia/Taipei', text: 'Taipei Standard Time (Asia/Taipei)' }, { value: 'Australia/Currie', text: 'Tasmania Standard Time (Australia/Currie)' }, { value: 'Australia/Hobart', text: 'Tasmania Standard Time (Australia/Hobart)' }, { value: 'Asia/Dili', text: 'Tokyo Standard Time (Asia/Dili)' }, { value: 'Asia/Jayapura', text: 'Tokyo Standard Time (Asia/Jayapura)' }, { value: 'Asia/Tokyo', text: 'Tokyo Standard Time (Asia/Tokyo)' }, { value: 'Etc/GMT-9', text: 'Tokyo Standard Time (Etc/GMT-9)' }, { value: 'Pacific/Palau', text: 'Tokyo Standard Time (Pacific/Palau)' }, { value: 'Etc/GMT-13', text: 'Tonga Standard Time (Etc/GMT-13)' }, { value: 'Pacific/Enderbury', text: 'Tonga Standard Time (Pacific/Enderbury)' }, { value: 'Pacific/Fakaofo', text: 'Tonga Standard Time (Pacific/Fakaofo)' }, { value: 'Pacific/Tongatapu', text: 'Tonga Standard Time (Pacific/Tongatapu)' }, { value: 'Europe/Istanbul', text: 'Turkey Standard Time (Europe/Istanbul)' }, { value: 'America/Indiana/Marengo', text: 'US Eastern Standard Time (America/Indiana/Marengo)' }, { value: 'America/Indiana/Vevay', text: 'US Eastern Standard Time (America/Indiana/Vevay)' }, { value: 'America/Indianapolis', text: 'US Eastern Standard Time (America/Indianapolis)' }, { value: 'America/Creston', text: 'US Mountain Standard Time (America/Creston)' }, { value: 'America/Dawson_Creek', text: 'US Mountain Standard Time (America/Dawson_Creek)' }, { value: 'America/Hermosillo', text: 'US Mountain Standard Time (America/Hermosillo)' }, { value: 'America/Phoenix', text: 'US Mountain Standard Time (America/Phoenix)' }, { value: 'Etc/GMT+7', text: 'US Mountain Standard Time (Etc/GMT+7)' }, { value: 'America/Danmarkshavn', text: 'UTC (America/Danmarkshavn)' }, { value: 'Etc/GMT', text: 'UTC (Etc/GMT)' }, { value: 'Etc/GMT-12', text: 'UTC+12 (Etc/GMT-12)' }, { value: 'Pacific/Funafuti', text: 'UTC+12 (Pacific/Funafuti)' }, { value: 'Pacific/Kwajalein', text: 'UTC+12 (Pacific/Kwajalein)' }, { value: 'Pacific/Majuro', text: 'UTC+12 (Pacific/Majuro)' }, { value: 'Pacific/Nauru', text: 'UTC+12 (Pacific/Nauru)' }, { value: 'Pacific/Tarawa', text: 'UTC+12 (Pacific/Tarawa)' }, { value: 'Pacific/Wake', text: 'UTC+12 (Pacific/Wake)' }, { value: 'Pacific/Wallis', text: 'UTC+12 (Pacific/Wallis)' }, { value: 'America/Noronha', text: 'UTC-02 (America/Noronha)' }, { value: 'Atlantic/South_Georgia', text: 'UTC-02 (Atlantic/South_Georgia)' }, { value: 'Etc/GMT+2', text: 'UTC-02 (Etc/GMT+2)' }, { value: 'Etc/GMT+11', text: 'UTC-11 (Etc/GMT+11)' }, { value: 'Pacific/Midway', text: 'UTC-11 (Pacific/Midway)' }, { value: 'Pacific/Niue', text: 'UTC-11 (Pacific/Niue)' }, { value: 'Pacific/Pago_Pago', text: 'UTC-11 (Pacific/Pago_Pago)' }, { value: 'Asia/Choibalsan', text: 'Ulaanbaatar Standard Time (Asia/Choibalsan)' }, { value: 'Asia/Ulaanbaatar', text: 'Ulaanbaatar Standard Time (Asia/Ulaanbaatar)' }, { value: 'America/Caracas', text: 'Venezuela Standard Time (America/Caracas)' }, { value: 'Asia/Sakhalin', text: 'Vladivostok Standard Time (Asia/Sakhalin)' }, { value: 'Asia/Ust-Nera', text: 'Vladivostok Standard Time (Asia/Ust-Nera)' }, { value: 'Asia/Vladivostok', text: 'Vladivostok Standard Time (Asia/Vladivostok)' }, { value: 'Antarctica/Casey', text: 'W. Australia Standard Time (Antarctica/Casey)' }, { value: 'Australia/Perth', text: 'W. Australia Standard Time (Australia/Perth)' }, { value: 'Africa/Algiers', text: 'W. Central Africa Standard Time (Africa/Algiers)' }, { value: 'Africa/Bangui', text: 'W. Central Africa Standard Time (Africa/Bangui)' }, { value: 'Africa/Brazzaville', text: 'W. Central Africa Standard Time (Africa/Brazzaville)' }, { value: 'Africa/Douala', text: 'W. Central Africa Standard Time (Africa/Douala)' }, { value: 'Africa/Kinshasa', text: 'W. Central Africa Standard Time (Africa/Kinshasa)' }, { value: 'Africa/Lagos', text: 'W. Central Africa Standard Time (Africa/Lagos)' }, { value: 'Africa/Libreville', text: 'W. Central Africa Standard Time (Africa/Libreville)' }, { value: 'Africa/Luanda', text: 'W. Central Africa Standard Time (Africa/Luanda)' }, { value: 'Africa/Malabo', text: 'W. Central Africa Standard Time (Africa/Malabo)' }, { value: 'Africa/Ndjamena', text: 'W. Central Africa Standard Time (Africa/Ndjamena)' }, { value: 'Africa/Niamey', text: 'W. Central Africa Standard Time (Africa/Niamey)' }, { value: 'Africa/Porto-Novo', text: 'W. Central Africa Standard Time (Africa/Porto-Novo)' }, { value: 'Africa/Tunis', text: 'W. Central Africa Standard Time (Africa/Tunis)' }, { value: 'Etc/GMT-1', text: 'W. Central Africa Standard Time (Etc/GMT-1)' }, { value: 'Arctic/Longyearbyen', text: 'W. Europe Standard Time (Arctic/Longyearbyen)' }, { value: 'Europe/Amsterdam', text: 'W. Europe Standard Time (Europe/Amsterdam)' }, { value: 'Europe/Andorra', text: 'W. Europe Standard Time (Europe/Andorra)' }, { value: 'Europe/Berlin', text: 'W. Europe Standard Time (Europe/Berlin)' }, { value: 'Europe/Busingen', text: 'W. Europe Standard Time (Europe/Busingen)' }, { value: 'Europe/Gibraltar', text: 'W. Europe Standard Time (Europe/Gibraltar)' }, { value: 'Europe/Luxembourg', text: 'W. Europe Standard Time (Europe/Luxembourg)' }, { value: 'Europe/Malta', text: 'W. Europe Standard Time (Europe/Malta)' }, { value: 'Europe/Monaco', text: 'W. Europe Standard Time (Europe/Monaco)' }, { value: 'Europe/Oslo', text: 'W. Europe Standard Time (Europe/Oslo)' }, { value: 'Europe/Rome', text: 'W. Europe Standard Time (Europe/Rome)' }, { value: 'Europe/San_Marino', text: 'W. Europe Standard Time (Europe/San_Marino)' }, { value: 'Europe/Stockholm', text: 'W. Europe Standard Time (Europe/Stockholm)' }, { value: 'Europe/Vaduz', text: 'W. Europe Standard Time (Europe/Vaduz)' }, { value: 'Europe/Vatican', text: 'W. Europe Standard Time (Europe/Vatican)' }, { value: 'Europe/Vienna', text: 'W. Europe Standard Time (Europe/Vienna)' }, { value: 'Europe/Zurich', text: 'W. Europe Standard Time (Europe/Zurich)' }, { value: 'Antarctica/Mawson', text: 'West Asia Standard Time (Antarctica/Mawson)' }, { value: 'Asia/Aqtau', text: 'West Asia Standard Time (Asia/Aqtau)' }, { value: 'Asia/Aqtobe', text: 'West Asia Standard Time (Asia/Aqtobe)' }, { value: 'Asia/Ashgabat', text: 'West Asia Standard Time (Asia/Ashgabat)' }, { value: 'Asia/Dushanbe', text: 'West Asia Standard Time (Asia/Dushanbe)' }, { value: 'Asia/Oral', text: 'West Asia Standard Time (Asia/Oral)' }, { value: 'Asia/Samarkand', text: 'West Asia Standard Time (Asia/Samarkand)' }, { value: 'Asia/Tashkent', text: 'West Asia Standard Time (Asia/Tashkent)' }, { value: 'Etc/GMT-5', text: 'West Asia Standard Time (Etc/GMT-5)' }, { value: 'Indian/Kerguelen', text: 'West Asia Standard Time (Indian/Kerguelen)' }, { value: 'Indian/Maldives', text: 'West Asia Standard Time (Indian/Maldives)' }, { value: 'Antarctica/DumontDUrville', text: 'West Pacific Standard Time (Antarctica/DumontDUrville)' }, { value: 'Etc/GMT-10', text: 'West Pacific Standard Time (Etc/GMT-10)' }, { value: 'Pacific/Guam', text: 'West Pacific Standard Time (Pacific/Guam)' }, { value: 'Pacific/Port_Moresby', text: 'West Pacific Standard Time (Pacific/Port_Moresby)' }, { value: 'Pacific/Saipan', text: 'West Pacific Standard Time (Pacific/Saipan)' }, { value: 'Pacific/Truk', text: 'West Pacific Standard Time (Pacific/Truk)' }, { value: 'Asia/Khandyga', text: 'Yakutsk Standard Time (Asia/Khandyga)' }, { value: 'Asia/Yakutsk', text: 'Yakutsk Standard Time (Asia/Yakutsk)' } ].filter((t) => { if (timezones) return timezones.indexOf(t.value) !== -1; else return true; }); const childrenWithProps = React.Children.map(children, (child) => React.cloneElement(child, { options, ...props }) ); return <div>{childrenWithProps}</div>; } TimezoneOptions.propTypes = { children: PropTypes.node.isRequired, timezones: PropTypes.arrayOf(PropTypes.string).isRequired }; export { TimezoneOptions }; ================================================ FILE: packages/evershop/src/components/common/modal/Alert.jsx ================================================ import { Button } from '@components/common/ui/Button.js'; import { assign } from '@evershop/evershop/lib/util/assign'; import { produce } from 'immer'; import PropTypes from 'prop-types'; import React, { useReducer } from 'react'; import ReactDOM from 'react-dom'; import './Alert.scss'; import { Card } from '@components/common/ui/Card'; import { CardContent, CardFooter, CardHeader, CardTitle } from '@components/common/ui/Card.js'; const AlertContext = React.createContext(); export const useAlertContext = () => React.useContext(AlertContext); function reducer(state, action) { switch (action.type) { case 'close': return { ...state, showing: false, closing: false }; case 'closing': return { ...state, showing: true, closing: true }; case 'open': return { ...state, showing: true, closing: false }; default: throw new Error(); } } const alertReducer = produce((draff, action) => { switch (action.type) { case 'open': draff = { ...action.payload }; return draff; case 'remove': return {}; case 'update': assign(draff, action.payload); return draff; default: throw new Error(); } }); function Alert({ children }) { const [alert, dispatchAlert] = useReducer(alertReducer, {}); const [state, dispatch] = useReducer(reducer, { showing: false, closing: false }); const openAlert = ({ heading, content, primaryAction, secondaryAction }) => { dispatchAlert({ type: 'open', payload: { heading, content, primaryAction, secondaryAction } }); dispatch({ type: 'open' }); }; return ( <AlertContext.Provider value={{ dispatchAlert, openAlert, closeAlert: () => dispatch({ type: 'closing' }) }} > {children} {state.showing === true && ReactDOM.createPortal( <div className={ state.closing === false ? 'modal-overlay fadeIn' : 'modal-overlay fadeOut' } onAnimationEnd={() => { if (state.closing) { dispatch({ type: 'close' }); dispatchAlert({ type: 'remove' }); } }} > <div key={state.key} className="modal-wrapper flex self-center justify-center" aria-modal aria-hidden tabIndex={-1} role="dialog" > <div className="modal"> <Card> {alert.heading && ( <CardHeader> <CardTitle>{alert.heading}</CardTitle> </CardHeader> )} <CardContent>{alert.content}</CardContent> {(alert.primaryAction !== undefined || alert.secondaryAction !== undefined) && ( <CardFooter> <div className="flex justify-end space-x-2 w-full"> {alert.primaryAction && ( <Button onClick={alert.primaryAction.onAction} variant={alert.primaryAction.variant} > {alert.primaryAction.title} </Button> )} {alert.secondaryAction && ( <Button onClick={alert.secondaryAction.onAction} variant={alert.secondaryAction.variant} > {alert.secondaryAction.title} </Button> )} </div> </CardFooter> )} </Card> </div> </div> </div>, document.body )} </AlertContext.Provider> ); } Alert.propTypes = { children: PropTypes.node.isRequired }; export { Alert }; ================================================ FILE: packages/evershop/src/components/common/modal/Alert.scss ================================================ .modal-overlay { position: fixed; top: 100%; left: 0; bottom: 0; right: 0; display: flex; justify-content: center; z-index: 1001; background-color: rgba(17, 18, 19, 0.3); -webkit-animation-duration: 0.25s; animation-duration: 0.25s; .modal-wrapper { max-height: calc(100vh - 4rem); .modal { max-height: calc(100vh - 4rem); } .modal-content { max-height: calc(100vh - 18rem); overflow-y: auto; } } &.fadeIn { top: 0; -webkit-animation-name: fadeIn; animation-name: fadeIn; } &.fadeOut { top: 100%; -webkit-animation-name: fadeOut; animation-name: fadeOut; } .modal { min-width: 31.25rem; max-width: 50%; position: relative; .modal-close-button { position: absolute; top: 0.938rem; right: 0.938rem; } } } @-webkit-keyframes fadeIn { 0% { top: 100%; opacity: 0; } 100% { top: 0%; opacity: 100; } } @keyframes fadeIn { 0% { top: 100%; opacity: 0; } 100% { top: 0%; opacity: 100; } } @-webkit-keyframes fadeOut { 0% { opacity: 100; top: 0%; } 100% { opacity: 0; top: 100%; } } @keyframes fadeOut { 0% { opacity: 100; top: 0%; } 100% { opacity: 0; top: 100%; } } ================================================ FILE: packages/evershop/src/components/common/react/Head.jsx ================================================ import Area from '@components/common/Area'; import React from 'react'; import ReactDOM from 'react-dom'; export default function Head() { return ReactDOM.createPortal( <Area id="head" noOuter />, document.getElementsByTagName('head')[0] ); } ================================================ FILE: packages/evershop/src/components/common/react/client/Client.tsx ================================================ import Area from '@components/common/Area.js'; import { AppProvider } from '@components/common/context/app.js'; import { Alert } from '@components/common/modal/Alert.js'; import Head from '@components/common/react/Head.js'; import React from 'react'; import { createClient, Provider } from 'urql'; declare global { interface Window { eContext: any; } } const client = createClient({ url: window.eContext?.config?.pageMeta?.route?.isAdmin ? '/api/admin/graphql' : '/api/graphql' }); interface AppProps { children: React.ReactNode; } export function App({ children }: AppProps) { return ( <AppProvider value={window.eContext}> <Provider value={client}> <Alert> <Head /> <Area id="body" className="wrapper" /> </Alert> </Provider> {children} </AppProvider> ); } ================================================ FILE: packages/evershop/src/components/common/react/client/HotReload.tsx ================================================ import { useAppDispatch, useAppState } from '@components/common/context/app.js'; import axios from 'axios'; import { produce } from 'immer'; import React from 'react'; interface HotReloadProps { hot: { subscribe: (callback: (event: { action: string }) => void) => void; }; } export function HotReload({ hot }: HotReloadProps): React.ReactElement | null { const [isRefreshing, setIsRefreshing] = React.useState(false); const appContext = useAppState(); const { setData } = useAppDispatch(); React.useEffect(() => { hot.subscribe(async (event) => { if (event.action === 'serverReloaded') { const interval = setInterval(async () => { try { const response = await axios.get('/', { validateStatus(status) { return status >= 200 && status <= 500; } }); if (response.status === 200) { // Server is back, reload the page window.location.reload(); clearInterval(interval); } } catch (error) { // Ignore errors, server might still be down } }, 300); } }); }, []); // React.useEffect(() => { // if (isRefreshing) { // // Use preflight request to check if server is back // const interval = setInterval(async () => { // try { // const response = await axios.get('/', { // validateStatus(status) { // return status >= 200 && status <= 500; // } // }); // if (response.status === 200) { // // Server is back, reload the page // window.location.reload(); // clearInterval(interval); // } // } catch (error) { // // Ignore errors, server might still be down // } // }, 300); // } // }, [isRefreshing]); return null; } ================================================ FILE: packages/evershop/src/components/common/react/client/Hydrate.tsx ================================================ import Area from '@components/common/Area.js'; import { AppProvider } from '@components/common/context/app.js'; import { Alert } from '@components/common/modal/Alert.js'; import React from 'react'; import { Client, Provider } from 'urql'; interface HydrateProps { client: Client; } export default function Hydrate({ client }: HydrateProps): React.ReactElement { return ( <Provider value={client}> <AppProvider value={window.eContext}> <Alert> <Area id="body" className="wrapper" /> </Alert> </AppProvider> </Provider> ); } ================================================ FILE: packages/evershop/src/components/common/react/client/HydrateAdmin.tsx ================================================ import React from 'react'; import { createClient } from 'urql'; import Hydrate from './Hydrate.js'; const client = createClient({ url: '/api/admin/graphql' }); export function HydrateAdmin() { return <Hydrate client={client} />; } ================================================ FILE: packages/evershop/src/components/common/react/client/HydrateFrontStore.tsx ================================================ import React from 'react'; import { createClient } from 'urql'; import Hydrate from './Hydrate.js'; const client = createClient({ url: '/api/graphql' }); export function HydrateFrontStore() { return <Hydrate client={client} />; } ================================================ FILE: packages/evershop/src/components/common/react/client/Index.jsx ================================================ import Area from '@components/common/Area'; import { App } from '@components/common/react/client/Client'; import { HotReload } from '@components/common/react/client/HotReload'; import React from 'react'; import ReactDOM from 'react-dom'; /** render */ ReactDOM.render( <App> <Area /> </App>, document.getElementById('app') ); ================================================ FILE: packages/evershop/src/components/common/react/getComponents.js ================================================ import { resolve } from 'path'; import { useAppState } from '@components/common/context/app'; import { CONSTANTS } from '../../../lib/helpers.js'; import { get } from '../../../lib/util/get.js'; export function getComponents() { const componentsPath = get(useAppState(), 'componentsPath'); if (!componentsPath) { return {}; } else { return require(resolve( CONSTANTS.ROOTPATH, '.evershop/build/', componentsPath )); } } ================================================ FILE: packages/evershop/src/components/common/react/server/Server.tsx ================================================ import Area from '@components/common/Area.js'; import { Alert } from '@components/common/modal/Alert.js'; import React from 'react'; import { Route } from '../../../../types/route.js'; interface ServerHtmlProps { route: Route; css: string[]; js: string[]; appContext: string; } function ServerHtml({ route, css, js, appContext }: ServerHtmlProps) { const classes = route.isAdmin ? `admin ${route.id}` : `frontStore ${route.id}`; return ( <> <head> <meta charSet="utf-8" /> <script dangerouslySetInnerHTML={{ __html: appContext }} /> {css.map((source, index) => ( <style key={index} dangerouslySetInnerHTML={{ __html: source }} /> ))} <Area noOuter id="head" /> </head> <body id="body" className={classes}> <div id="app"> <Alert> <Area id="body" className="wrapper" /> </Alert> </div> {js.map((src, index) => ( <script src={src} key={index} /> ))} </body> </> ); } export default ServerHtml; ================================================ FILE: packages/evershop/src/components/common/react/server/render.tsx ================================================ import { AppProvider } from '@components/common/context/app.js'; import ServerHtml from '@components/common/react/server/Server.js'; import React from 'react'; import { renderToString } from 'react-dom/server.js'; function renderHtml(route, js, css, contextData, langeCode) { const source = renderToString( <AppProvider value={JSON.parse(contextData)}> <ServerHtml route={route} js={js} css={css} appContext={`var eContext = ${contextData}`} /> </AppProvider> ); return `<!DOCTYPE html><html id="root" lang="${langeCode}">${source}</html>`; } export { renderHtml }; ================================================ FILE: packages/evershop/src/components/common/ui/Accordion.tsx ================================================ import { Accordion as AccordionPrimitive } from '@base-ui/react/accordion'; import { cn } from '@evershop/evershop/lib/util/cn'; import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react'; import React from 'react'; function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) { return ( <AccordionPrimitive.Root data-slot="accordion" className={cn('flex w-full flex-col', className)} {...props} /> ); } function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) { return ( <AccordionPrimitive.Item data-slot="accordion-item" className={cn('not-last:border-b', className)} {...props} /> ); } function AccordionTrigger({ className, children, ...props }: AccordionPrimitive.Trigger.Props) { return ( <AccordionPrimitive.Header className="flex"> <AccordionPrimitive.Trigger data-slot="accordion-trigger" className={cn( 'focus-visible:ring-ring/50 focus-visible:border-ring focus-visible:after:border-ring **:data-[slot=accordion-trigger-icon]:text-muted-foreground rounded-md py-4 text-left text-sm font-medium hover:underline focus-visible:ring-[3px] **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 group/accordion-trigger relative flex flex-1 items-start justify-between border border-transparent transition-all outline-none disabled:pointer-events-none disabled:opacity-50', className )} {...props} > {children} <ChevronDownIcon data-slot="accordion-trigger-icon" className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden" /> <ChevronUpIcon data-slot="accordion-trigger-icon" className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline" /> </AccordionPrimitive.Trigger> </AccordionPrimitive.Header> ); } function AccordionContent({ className, children, ...props }: AccordionPrimitive.Panel.Props) { return ( <AccordionPrimitive.Panel data-slot="accordion-content" className="data-open:animate-accordion-down data-closed:animate-accordion-up text-sm overflow-hidden" {...props} > <div className={cn( 'pt-0 pb-4 [&_a]:hover:text-foreground h-(--accordion-panel-height) data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4', className )} > {children} </div> </AccordionPrimitive.Panel> ); } export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; ================================================ FILE: packages/evershop/src/components/common/ui/Alert.tsx ================================================ import { cn } from '@evershop/evershop/lib/util/cn'; import { cva, type VariantProps } from 'class-variance-authority'; import * as React from 'react'; const alertVariants = cva( "grid gap-0.5 rounded-lg border px-4 py-3 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4 w-full relative group/alert", { variants: { variant: { default: 'bg-card text-card-foreground', destructive: 'text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current' } }, defaultVariants: { variant: 'default' } } ); function Alert({ className, variant, ...props }: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) { return ( <div data-slot="alert" role="alert" className={cn(alertVariants({ variant }), className)} {...props} /> ); } function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="alert-title" className={cn( 'font-medium group-has-[>svg]/alert:col-start-2 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3', className )} {...props} /> ); } function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="alert-description" className={cn( 'text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3', className )} {...props} /> ); } function AlertAction({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="alert-action" className={cn('absolute top-2.5 right-3', className)} {...props} /> ); } export { Alert, AlertTitle, AlertDescription, AlertAction }; ================================================ FILE: packages/evershop/src/components/common/ui/AlertDialog.tsx ================================================ import { AlertDialog as AlertDialogPrimitive } from '@base-ui/react/alert-dialog'; import { Button } from '@components/common/ui/Button.js'; import { cn } from '@evershop/evershop/lib/util/cn'; import * as React from 'react'; function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) { return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />; } function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) { return ( <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} /> ); } function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) { return ( <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} /> ); } function AlertDialogOverlay({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) { return ( <AlertDialogPrimitive.Backdrop data-slot="alert-dialog-overlay" className={cn( 'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50', className )} {...props} /> ); } function AlertDialogContent({ className, size = 'default', ...props }: AlertDialogPrimitive.Popup.Props & { size?: 'default' | 'sm'; }) { return ( <AlertDialogPortal> <AlertDialogOverlay /> <AlertDialogPrimitive.Popup data-slot="alert-dialog-content" data-size={size} className={cn( 'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background ring-foreground/10 gap-6 rounded-xl p-6 ring-1 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 outline-none', className )} {...props} /> </AlertDialogPortal> ); } function AlertDialogHeader({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="alert-dialog-header" className={cn( 'grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]', className )} {...props} /> ); } function AlertDialogFooter({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="alert-dialog-footer" className={cn( 'flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end', className )} {...props} /> ); } function AlertDialogMedia({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="alert-dialog-media" className={cn( "bg-muted mb-2 inline-flex size-16 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-8", className )} {...props} /> ); } function AlertDialogTitle({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) { return ( <AlertDialogPrimitive.Title data-slot="alert-dialog-title" className={cn( 'text-lg font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2', className )} {...props} /> ); } function AlertDialogDescription({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) { return ( <AlertDialogPrimitive.Description data-slot="alert-dialog-description" className={cn( 'text-muted-foreground *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3', className )} {...props} /> ); } function AlertDialogAction({ className, ...props }: React.ComponentProps<typeof Button>) { return ( <Button data-slot="alert-dialog-action" className={cn(className)} {...props} /> ); } function AlertDialogCancel({ className, variant = 'outline', size = 'default', ...props }: AlertDialogPrimitive.Close.Props & Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) { return ( <AlertDialogPrimitive.Close data-slot="alert-dialog-cancel" className={cn(className)} render={<Button variant={variant} size={size} />} {...props} /> ); } export { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogMedia, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger }; ================================================ FILE: packages/evershop/src/components/common/ui/AspectRatio.tsx ================================================ import { cn } from '@evershop/evershop/lib/util/cn'; import React from 'react'; function AspectRatio({ ratio, className, ...props }: React.ComponentProps<'div'> & { ratio: number }) { return ( <div data-slot="aspect-ratio" style={ { '--ratio': ratio } as React.CSSProperties } className={cn('relative aspect-(--ratio)', className)} {...props} /> ); } export { AspectRatio }; ================================================ FILE: packages/evershop/src/components/common/ui/Avatar.tsx ================================================ import { Avatar as AvatarPrimitive } from '@base-ui/react/avatar'; import { cn } from '@evershop/evershop/lib/util/cn'; import * as React from 'react'; function Avatar({ className, size = 'default', ...props }: AvatarPrimitive.Root.Props & { size?: 'default' | 'sm' | 'lg'; }) { return ( <AvatarPrimitive.Root data-slot="avatar" data-size={size} className={cn( 'size-8 rounded-full after:rounded-full data-[size=lg]:size-10 data-[size=sm]:size-6 after:border-border group/avatar relative flex shrink-0 select-none after:absolute after:inset-0 after:border after:mix-blend-darken dark:after:mix-blend-lighten', className )} {...props} /> ); } function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) { return ( <AvatarPrimitive.Image data-slot="avatar-image" className={cn( 'rounded-full aspect-square size-full object-cover', className )} {...props} /> ); } function AvatarFallback({ className, ...props }: AvatarPrimitive.Fallback.Props) { return ( <AvatarPrimitive.Fallback data-slot="avatar-fallback" className={cn( 'bg-muted text-muted-foreground rounded-full flex size-full items-center justify-center text-sm group-data-[size=sm]/avatar:text-xs', className )} {...props} /> ); } function AvatarBadge({ className, ...props }: React.ComponentProps<'span'>) { return ( <span data-slot="avatar-badge" className={cn( 'bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none', 'group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden', 'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2', 'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2', className )} {...props} /> ); } function AvatarGroup({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="avatar-group" className={cn( '*:data-[slot=avatar]:ring-background group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2', className )} {...props} /> ); } function AvatarGroupCount({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="avatar-group-count" className={cn( 'bg-muted text-muted-foreground size-8 rounded-full text-sm group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 ring-background relative flex shrink-0 items-center justify-center ring-2', className )} {...props} /> ); } export { Avatar, AvatarImage, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarBadge }; ================================================ FILE: packages/evershop/src/components/common/ui/Badge.tsx ================================================ import { mergeProps } from '@base-ui/react/merge-props'; import { useRender } from '@base-ui/react/use-render'; import { cn } from '@evershop/evershop/lib/util/cn'; import { cva, type VariantProps } from 'class-variance-authority'; import React from 'react'; const badgeVariants = cva( 'h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-colors overflow-hidden group/badge', { variants: { variant: { default: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground', secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80', destructive: 'bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20', success: 'bg-green-500/10 [a]:hover:bg-green-500/20 focus-visible:ring-green-500/20 dark:focus-visible:ring-green-500/40 text-green-700 dark:text-green-400 dark:bg-green-500/20', warning: 'bg-amber-500/10 [a]:hover:bg-amber-500/20 focus-visible:ring-amber-500/20 dark:focus-visible:ring-amber-500/40 text-amber-700 dark:text-amber-400 dark:bg-amber-500/20', outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground', ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50', link: 'text-primary underline-offset-4 hover:underline' } }, defaultVariants: { variant: 'default' } } ); function Badge({ className, variant = 'default', render, ...props }: useRender.ComponentProps<'span'> & VariantProps<typeof badgeVariants>) { return useRender({ defaultTagName: 'span', props: mergeProps<'span'>( { className: cn(badgeVariants({ className, variant })) }, props ), render, state: { slot: 'badge', variant } }); } export { Badge, badgeVariants }; ================================================ FILE: packages/evershop/src/components/common/ui/Breadcrumb.tsx ================================================ import { mergeProps } from '@base-ui/react/merge-props'; import { useRender } from '@base-ui/react/use-render'; import { cn } from '@evershop/evershop/lib/util/cn'; import { ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react'; import * as React from 'react'; function Breadcrumb({ className, ...props }: React.ComponentProps<'nav'>) { return ( <nav aria-label="breadcrumb" data-slot="breadcrumb" className={cn(className)} {...props} /> ); } function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) { return ( <ol data-slot="breadcrumb-list" className={cn( 'text-muted-foreground gap-1.5 text-sm sm:gap-2.5 flex flex-wrap items-center break-words', className )} {...props} /> ); } function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) { return ( <li data-slot="breadcrumb-item" className={cn('gap-1.5 inline-flex items-center', className)} {...props} /> ); } function BreadcrumbLink({ className, render, ...props }: useRender.ComponentProps<'a'>) { return useRender({ defaultTagName: 'a', props: mergeProps<'a'>( { className: cn('hover:text-foreground transition-colors', className) }, props ), render, state: { slot: 'breadcrumb-link' } }); } function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) { return ( <span data-slot="breadcrumb-page" role="link" aria-disabled="true" aria-current="page" className={cn('text-foreground font-normal', className)} {...props} /> ); } function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<'li'>) { return ( <li data-slot="breadcrumb-separator" role="presentation" aria-hidden="true" className={cn('[&>svg]:size-3.5', className)} {...props} > {children ?? <ChevronRightIcon />} </li> ); } function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<'span'>) { return ( <span data-slot="breadcrumb-ellipsis" role="presentation" aria-hidden="true" className={cn( 'size-5 [&>svg]:size-4 flex items-center justify-center', className )} {...props} > <MoreHorizontalIcon /> <span className="sr-only">More</span> </span> ); } export { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator, BreadcrumbEllipsis }; ================================================ FILE: packages/evershop/src/components/common/ui/Button.tsx ================================================ import { Button as ButtonPrimitive } from '@base-ui/react/button'; import { cn } from '@evershop/evershop/lib/util/cn'; import { cva, type VariantProps } from 'class-variance-authority'; import React from 'react'; import { Spinner } from './Spinner.js'; const buttonVariants = cva( "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none", { variants: { variant: { default: 'bg-primary text-primary-foreground hover:bg-primary/80', outline: 'border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground', ghost: 'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground', destructive: 'bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30', link: 'text-primary underline-offset-4 hover:underline' }, size: { default: 'h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", sm: 'h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5', lg: 'h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3', xl: "h-12 gap-2 px-4 text-base has-data-[icon=inline-end]:pr-3.5 has-data-[icon=inline-start]:pl-3.5 [&_svg:not([class*='size-'])]:size-5", icon: 'size-9', 'icon-xs': "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3", 'icon-sm': 'size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md', 'icon-lg': 'size-10', 'icon-xl': 'size-12' } }, defaultVariants: { variant: 'default', size: 'default' } } ); function Button({ className, variant = 'default', size = 'default', isLoading = false, disabled, children, ...props }: ButtonPrimitive.Props & VariantProps<typeof buttonVariants> & { isLoading?: boolean; }) { return ( <ButtonPrimitive data-slot="button" className={cn(buttonVariants({ variant, size, className }))} disabled={disabled || isLoading} {...props} > {isLoading ? <Spinner /> : children} </ButtonPrimitive> ); } export { Button, buttonVariants }; ================================================ FILE: packages/evershop/src/components/common/ui/ButtonGroup.tsx ================================================ import { mergeProps } from '@base-ui/react/merge-props'; import { useRender } from '@base-ui/react/use-render'; import { Separator } from '@components/common/ui/Separator.js'; import { cn } from '@evershop/evershop/lib/util/cn'; import { cva, type VariantProps } from 'class-variance-authority'; import React from 'react'; const buttonGroupVariants = cva( "has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1", { variants: { orientation: { horizontal: '[&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-md! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0 [&>[data-slot]]:rounded-r-none', vertical: '[&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-md! flex-col [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0 [&>[data-slot]]:rounded-b-none' } }, defaultVariants: { orientation: 'horizontal' } } ); function ButtonGroup({ className, orientation, ...props }: React.ComponentProps<'div'> & VariantProps<typeof buttonGroupVariants>) { return ( <div role="group" data-slot="button-group" data-orientation={orientation} className={cn(buttonGroupVariants({ orientation }), className)} {...props} /> ); } function ButtonGroupText({ className, render, ...props }: useRender.ComponentProps<'div'>) { return useRender({ defaultTagName: 'div', props: mergeProps<'div'>( { className: cn( "bg-muted gap-2 rounded-md border px-2.5 text-sm font-medium shadow-xs [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none", className ) }, props ), render, state: { slot: 'button-group-text' } }); } function ButtonGroupSeparator({ className, orientation = 'vertical', ...props }: React.ComponentProps<typeof Separator>) { return ( <Separator data-slot="button-group-separator" orientation={orientation} className={cn( 'bg-input relative self-stretch data-[orientation=horizontal]:mx-px data-[orientation=horizontal]:w-auto data-[orientation=vertical]:my-px data-[orientation=vertical]:h-auto', className )} {...props} /> ); } export { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, buttonGroupVariants }; ================================================ FILE: packages/evershop/src/components/common/ui/Card.tsx ================================================ import { cn } from '@evershop/evershop/lib/util/cn'; import * as React from 'react'; function Card({ className, size = 'default', ...props }: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }) { return ( <div data-slot="card" data-size={size} className={cn( 'ring-foreground/10 bg-card text-card-foreground gap-6 rounded-xl py-6 text-sm shadow-xs ring-1 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col', className )} {...props} /> ); } function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="card-header" className={cn( 'gap-1 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]', className )} {...props} /> ); } function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="card-title" className={cn( 'text-base leading-normal font-medium group-data-[size=sm]/card:text-sm', className )} {...props} /> ); } function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="card-description" className={cn('text-muted-foreground text-sm', className)} {...props} /> ); } function CardAction({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="card-action" className={cn( 'col-start-2 row-span-2 row-start-1 self-start justify-self-end', className )} {...props} /> ); } function CardContent({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="card-content" className={cn('px-6 group-data-[size=sm]/card:px-4', className)} {...props} /> ); } function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="card-footer" className={cn( 'rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4 flex items-center', className )} {...props} /> ); } export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }; ================================================ FILE: packages/evershop/src/components/common/ui/Chart.tsx ================================================ import { cn } from '@evershop/evershop/lib/util/cn'; import * as React from 'react'; import * as RechartsPrimitive from 'recharts'; // Format: { THEME_NAME: CSS_SELECTOR } const THEMES = { light: '', dark: '.dark' } as const; export type ChartConfig = { [k in string]: { label?: React.ReactNode; icon?: React.ComponentType; } & ( | { color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> } ); }; type ChartContextProps = { config: ChartConfig; }; const ChartContext = React.createContext<ChartContextProps | null>(null); function useChart() { const context = React.useContext(ChartContext); if (!context) { throw new Error('useChart must be used within a <ChartContainer />'); } return context; } function ChartContainer({ id, className, children, config, ...props }: React.ComponentProps<'div'> & { config: ChartConfig; children: React.ComponentProps< typeof RechartsPrimitive.ResponsiveContainer >['children']; }) { const uniqueId = React.useId(); const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`; return ( <ChartContext.Provider value={{ config }}> <div data-slot="chart" data-chart={chartId} className={cn( "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden", className )} {...props} > <ChartStyle id={chartId} config={config} /> <RechartsPrimitive.ResponsiveContainer> {children} </RechartsPrimitive.ResponsiveContainer> </div> </ChartContext.Provider> ); } const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { const colorConfig = Object.entries(config).filter( ([, config]) => config.theme || config.color ); if (!colorConfig.length) { return null; } return ( <style dangerouslySetInnerHTML={{ __html: Object.entries(THEMES) .map( ([theme, prefix]) => ` ${prefix} [data-chart=${id}] { ${colorConfig .map(([key, itemConfig]) => { const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color; return color ? ` --color-${key}: ${color};` : null; }) .join('\n')} } ` ) .join('\n') }} /> ); }; const ChartTooltip = RechartsPrimitive.Tooltip; function ChartTooltipContent({ active, payload, className, indicator = 'dot', hideLabel = false, hideIndicator = false, label, labelFormatter, labelClassName, formatter, color, nameKey, labelKey }: React.ComponentProps<typeof RechartsPrimitive.Tooltip> & React.ComponentProps<'div'> & { hideLabel?: boolean; hideIndicator?: boolean; indicator?: 'line' | 'dot' | 'dashed'; nameKey?: string; labelKey?: string; }) { const { config } = useChart(); const tooltipLabel = React.useMemo(() => { if (hideLabel || !payload?.length) { return null; } const [item] = payload; const key = `${labelKey || item?.dataKey || item?.name || 'value'}`; const itemConfig = getPayloadConfigFromPayload(config, item, key); const value = !labelKey && typeof label === 'string' ? config[label as keyof typeof config]?.label || label : itemConfig?.label; if (labelFormatter) { return ( <div className={cn('font-medium', labelClassName)}> {labelFormatter(value, payload)} </div> ); } if (!value) { return null; } return <div className={cn('font-medium', labelClassName)}>{value}</div>; }, [ label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey ]); if (!active || !payload?.length) { return null; } const nestLabel = payload.length === 1 && indicator !== 'dot'; return ( <div className={cn( 'border-border/50 bg-background gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl grid min-w-[8rem] items-start', className )} > {!nestLabel ? tooltipLabel : null} <div className="grid gap-1.5"> {payload .filter((item) => item.type !== 'none') .map((item, index) => { const key = `${nameKey || item.name || item.dataKey || 'value'}`; const itemConfig = getPayloadConfigFromPayload(config, item, key); const indicatorColor = color || item.payload.fill || item.color; return ( <div key={item.dataKey} className={cn( '[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5', indicator === 'dot' && 'items-center' )} > {formatter && item?.value !== undefined && item.name ? ( formatter(item.value, item.name, item, index, item.payload) ) : ( <> {itemConfig?.icon ? ( <itemConfig.icon /> ) : ( !hideIndicator && ( <div className={cn( 'shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)', { 'h-2.5 w-2.5': indicator === 'dot', 'w-1': indicator === 'line', 'w-0 border-[1.5px] border-dashed bg-transparent': indicator === 'dashed', 'my-0.5': nestLabel && indicator === 'dashed' } )} style={ { '--color-bg': indicatorColor, '--color-border': indicatorColor } as React.CSSProperties } /> ) )} <div className={cn( 'flex flex-1 justify-between leading-none', nestLabel ? 'items-end' : 'items-center' )} > <div className="grid gap-1.5"> {nestLabel ? tooltipLabel : null} <span className="text-muted-foreground"> {itemConfig?.label || item.name} </span> </div> {item.value && ( <span className="text-foreground font-mono font-medium tabular-nums"> {item.value.toLocaleString()} </span> )} </div> </> )} </div> ); })} </div> </div> ); } const ChartLegend = RechartsPrimitive.Legend; function ChartLegendContent({ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey }: React.ComponentProps<'div'> & Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & { hideIcon?: boolean; nameKey?: string; }) { const { config } = useChart(); if (!payload?.length) { return null; } return ( <div className={cn( 'flex items-center justify-center gap-4', verticalAlign === 'top' ? 'pb-3' : 'pt-3', className )} > {payload .filter((item) => item.type !== 'none') .map((item) => { const key = `${nameKey || item.dataKey || 'value'}`; const itemConfig = getPayloadConfigFromPayload(config, item, key); return ( <div key={item.value} className={cn( '[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3' )} > {itemConfig?.icon && !hideIcon ? ( <itemConfig.icon /> ) : ( <div className="h-2 w-2 shrink-0 rounded-[2px]" style={{ backgroundColor: item.color }} /> )} {itemConfig?.label} </div> ); })} </div> ); } function getPayloadConfigFromPayload( config: ChartConfig, payload: unknown, key: string ) { if (typeof payload !== 'object' || payload === null) { return undefined; } const payloadPayload = 'payload' in payload && typeof payload.payload === 'object' && payload.payload !== null ? payload.payload : undefined; let configLabelKey: string = key; if ( key in payload && typeof payload[key as keyof typeof payload] === 'string' ) { configLabelKey = payload[key as keyof typeof payload] as string; } else if ( payloadPayload && key in payloadPayload && typeof payloadPayload[key as keyof typeof payloadPayload] === 'string' ) { configLabelKey = payloadPayload[ key as keyof typeof payloadPayload ] as string; } return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]; } export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle }; ================================================ FILE: packages/evershop/src/components/common/ui/Checkbox.tsx ================================================ import { Checkbox as CheckboxPrimitive } from '@base-ui/react/checkbox'; import { cn } from '@evershop/evershop/lib/util/cn'; import { CheckIcon } from 'lucide-react'; import React from 'react'; function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) { return ( <CheckboxPrimitive.Root data-slot="checkbox" className={cn( 'border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-[4px] border shadow-xs transition-shadow group-has-disabled/field:opacity-50 focus-visible:ring-[3px] aria-invalid:ring-[3px] peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50', className )} {...props} > <CheckboxPrimitive.Indicator data-slot="checkbox-indicator" className="[&>svg]:size-3.5 grid place-content-center text-current transition-none" > <CheckIcon /> </CheckboxPrimitive.Indicator> </CheckboxPrimitive.Root> ); } export { Checkbox }; ================================================ FILE: packages/evershop/src/components/common/ui/Circle.tsx ================================================ import { cn } from '@evershop/evershop/lib/util/cn'; import { cva } from 'class-variance-authority'; import React from 'react'; const circleVariants = cva('rounded-full inline-flex border-4', { variants: { variant: { default: 'border-muted', success: 'border-green-500/30', info: 'border-cyan-500/30', attention: 'border-yellow-500/30', destructive: 'border-destructive/30', warning: 'border-amber-500/30', new: 'border-muted' } }, defaultVariants: { variant: 'default' } }); const circleInnerVariants = cva( 'border-2 rounded-full block w-[1.125rem] h-[1.125rem] flex justify-center', { variants: { variant: { default: 'border-muted-foreground', success: 'border-green-700 dark:border-green-400', info: 'border-cyan-600 dark:border-cyan-400', attention: 'border-yellow-700 dark:border-yellow-400', destructive: 'border-destructive', warning: 'border-amber-700 dark:border-amber-400', new: 'border-muted-foreground' } }, defaultVariants: { variant: 'default' } } ); const circleDotVariants = cva( 'block w-[40%] h-[40%] self-center rounded-full', { variants: { variant: { default: 'bg-muted-foreground', success: 'bg-green-700 dark:bg-green-400', info: 'bg-cyan-600 dark:bg-cyan-400', attention: 'bg-yellow-700 dark:bg-yellow-400', destructive: 'bg-destructive', warning: 'bg-amber-700 dark:bg-amber-400', new: 'bg-muted-foreground' } }, defaultVariants: { variant: 'default' } } ); export type CircleVariant = | 'default' | 'success' | 'info' | 'attention' | 'destructive' | 'warning' | 'new'; export interface CircleProps { variant?: CircleVariant; className?: string; } export function Circle({ variant = 'default', className }: CircleProps) { return ( <span className={cn(circleVariants({ variant }), className)}> <span className={circleInnerVariants({ variant })}> <span className={circleDotVariants({ variant })} /> </span> </span> ); } ================================================ FILE: packages/evershop/src/components/common/ui/Collapsible.tsx ================================================ import { Collapsible as CollapsiblePrimitive } from '@base-ui/react/collapsible'; import React from 'react'; function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) { return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />; } function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) { return ( <CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} /> ); } function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) { return ( <CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} /> ); } export { Collapsible, CollapsibleTrigger, CollapsibleContent }; ================================================ FILE: packages/evershop/src/components/common/ui/ContextMenu.tsx ================================================ import { ContextMenu as ContextMenuPrimitive } from '@base-ui/react/context-menu'; import { cn } from '@evershop/evershop/lib/util/cn'; import { ChevronRightIcon, CheckIcon } from 'lucide-react'; import * as React from 'react'; function ContextMenu({ ...props }: ContextMenuPrimitive.Root.Props) { return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />; } function ContextMenuPortal({ ...props }: ContextMenuPrimitive.Portal.Props) { return ( <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} /> ); } function ContextMenuTrigger({ className, ...props }: ContextMenuPrimitive.Trigger.Props) { return ( <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" className={cn('select-none', className)} {...props} /> ); } function ContextMenuContent({ className, align = 'start', alignOffset = 4, side = 'right', sideOffset = 0, ...props }: ContextMenuPrimitive.Popup.Props & Pick< ContextMenuPrimitive.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset' >) { return ( <ContextMenuPrimitive.Portal> <ContextMenuPrimitive.Positioner className="isolate z-50 outline-none" align={align} alignOffset={alignOffset} side={side} sideOffset={sideOffset} > <ContextMenuPrimitive.Popup data-slot="context-menu-content" className={cn( 'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-36 rounded-md p-1 shadow-md ring-1 duration-100 z-50 max-h-(--available-height) origin-(--transform-origin) overflow-x-hidden overflow-y-auto outline-none', className )} {...props} /> </ContextMenuPrimitive.Positioner> </ContextMenuPrimitive.Portal> ); } function ContextMenuGroup({ ...props }: ContextMenuPrimitive.Group.Props) { return ( <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} /> ); } function ContextMenuLabel({ className, inset, ...props }: ContextMenuPrimitive.GroupLabel.Props & { inset?: boolean; }) { return ( <ContextMenuPrimitive.GroupLabel data-slot="context-menu-label" data-inset={inset} className={cn( 'text-muted-foreground px-2 py-1.5 text-xs font-medium data-[inset]:pl-8', className )} {...props} /> ); } function ContextMenuItem({ className, inset, variant = 'default', ...props }: ContextMenuPrimitive.Item.Props & { inset?: boolean; variant?: 'default' | 'destructive'; }) { return ( <ContextMenuPrimitive.Item data-slot="context-menu-item" data-inset={inset} data-variant={variant} className={cn( "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive focus:*:[svg]:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 group/context-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0", className )} {...props} /> ); } function ContextMenuSub({ ...props }: ContextMenuPrimitive.SubmenuRoot.Props) { return ( <ContextMenuPrimitive.SubmenuRoot data-slot="context-menu-sub" {...props} /> ); } function ContextMenuSubTrigger({ className, inset, children, ...props }: ContextMenuPrimitive.SubmenuTrigger.Props & { inset?: boolean; }) { return ( <ContextMenuPrimitive.SubmenuTrigger data-slot="context-menu-sub-trigger" data-inset={inset} className={cn( "focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground rounded-sm px-2 py-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0", className )} {...props} > {children} <ChevronRightIcon className="ml-auto" /> </ContextMenuPrimitive.SubmenuTrigger> ); } function ContextMenuSubContent({ ...props }: React.ComponentProps<typeof ContextMenuContent>) { return ( <ContextMenuContent data-slot="context-menu-sub-content" className="shadow-lg" side="right" {...props} /> ); } function ContextMenuCheckboxItem({ className, children, checked, ...props }: ContextMenuPrimitive.CheckboxItem.Props) { return ( <ContextMenuPrimitive.CheckboxItem data-slot="context-menu-checkbox-item" className={cn( "focus:bg-accent focus:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", className )} checked={checked} {...props} > <span className="absolute right-2 pointer-events-none"> <ContextMenuPrimitive.CheckboxItemIndicator> <CheckIcon /> </ContextMenuPrimitive.CheckboxItemIndicator> </span> {children} </ContextMenuPrimitive.CheckboxItem> ); } function ContextMenuRadioGroup({ ...props }: ContextMenuPrimitive.RadioGroup.Props) { return ( <ContextMenuPrimitive.RadioGroup data-slot="context-menu-radio-group" {...props} /> ); } function ContextMenuRadioItem({ className, children, ...props }: ContextMenuPrimitive.RadioItem.Props) { return ( <ContextMenuPrimitive.RadioItem data-slot="context-menu-radio-item" className={cn( "focus:bg-accent focus:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", className )} {...props} > <span className="absolute right-2 pointer-events-none"> <ContextMenuPrimitive.RadioItemIndicator> <CheckIcon /> </ContextMenuPrimitive.RadioItemIndicator> </span> {children} </ContextMenuPrimitive.RadioItem> ); } function ContextMenuSeparator({ className, ...props }: ContextMenuPrimitive.Separator.Props) { return ( <ContextMenuPrimitive.Separator data-slot="context-menu-separator" className={cn('bg-border -mx-1 my-1 h-px', className)} {...props} /> ); } function ContextMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) { return ( <span data-slot="context-menu-shortcut" className={cn( 'text-muted-foreground group-focus/context-menu-item:text-accent-foreground ml-auto text-xs tracking-widest', className )} {...props} /> ); } export { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuCheckboxItem, ContextMenuRadioItem, ContextMenuLabel, ContextMenuSeparator, ContextMenuShortcut, ContextMenuGroup, ContextMenuPortal, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuRadioGroup }; ================================================ FILE: packages/evershop/src/components/common/ui/Dialog.tsx ================================================ import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'; import { Button } from '@components/common/ui/Button.js'; import { cn } from '@evershop/evershop/lib/util/cn'; import { XIcon } from 'lucide-react'; import * as React from 'react'; function Dialog({ ...props }: DialogPrimitive.Root.Props) { return <DialogPrimitive.Root data-slot="dialog" {...props} />; } function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) { return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />; } function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) { return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />; } function DialogClose({ ...props }: DialogPrimitive.Close.Props) { return <DialogPrimitive.Close data-slot="dialog-close" {...props} />; } function DialogOverlay({ className, ...props }: DialogPrimitive.Backdrop.Props) { return ( <DialogPrimitive.Backdrop data-slot="dialog-overlay" className={cn( 'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50', className )} {...props} /> ); } function DialogContent({ className, children, showCloseButton = true, ...props }: DialogPrimitive.Popup.Props & { showCloseButton?: boolean; }) { return ( <DialogPortal> <DialogOverlay /> <DialogPrimitive.Popup data-slot="dialog-content" className={cn( 'bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-6 rounded-xl p-6 text-sm ring-1 duration-100 sm:max-w-md fixed top-1/2 left-1/2 z-1001 w-full -translate-x-1/2 -translate-y-1/2 outline-none', className )} {...props} > {children} {showCloseButton && ( <DialogPrimitive.Close data-slot="dialog-close" render={ <Button variant="ghost" className="absolute top-4 right-4" size="icon-sm" /> } > <XIcon /> <span className="sr-only">Close</span> </DialogPrimitive.Close> )} </DialogPrimitive.Popup> </DialogPortal> ); } function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="dialog-header" className={cn('gap-2 flex flex-col', className)} {...props} /> ); } function DialogFooter({ className, showCloseButton = false, children, ...props }: React.ComponentProps<'div'> & { showCloseButton?: boolean; }) { return ( <div data-slot="dialog-footer" className={cn( 'gap-2 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className )} {...props} > {children} {showCloseButton && ( <DialogPrimitive.Close render={<Button variant="outline" />}> Close </DialogPrimitive.Close> )} </div> ); } function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) { return ( <DialogPrimitive.Title data-slot="dialog-title" className={cn('leading-none font-medium', className)} {...props} /> ); } function DialogDescription({ className, ...props }: DialogPrimitive.Description.Props) { return ( <DialogPrimitive.Description data-slot="dialog-description" className={cn( 'text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3', className )} {...props} /> ); } export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger }; ================================================ FILE: packages/evershop/src/components/common/ui/DropdownMenu.tsx ================================================ import { Menu as MenuPrimitive } from '@base-ui/react/menu'; import { cn } from '@evershop/evershop/lib/util/cn'; import { ChevronRightIcon, CheckIcon } from 'lucide-react'; import * as React from 'react'; function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) { return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />; } function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) { return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />; } function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) { return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />; } function DropdownMenuContent({ align = 'start', alignOffset = 0, side = 'bottom', sideOffset = 4, className, ...props }: MenuPrimitive.Popup.Props & Pick< MenuPrimitive.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset' >) { return ( <MenuPrimitive.Portal> <MenuPrimitive.Positioner className="isolate z-50 outline-none" align={align} alignOffset={alignOffset} side={side} sideOffset={sideOffset} > <MenuPrimitive.Popup data-slot="dropdown-menu-content" className={cn( 'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-md p-1 shadow-md ring-1 duration-100 z-50 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden', className )} {...props} /> </MenuPrimitive.Positioner> </MenuPrimitive.Portal> ); } function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) { return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />; } function DropdownMenuLabel({ className, inset, ...props }: React.ComponentProps<'div'> & { inset?: boolean; }) { return ( <div data-slot="dropdown-menu-label" data-inset={inset} className={cn( 'text-muted-foreground px-2 py-1.5 text-xs font-medium data-inset:pl-8', className )} {...props} /> ); } function DropdownMenuItem({ className, inset, variant = 'default', ...props }: MenuPrimitive.Item.Props & { inset?: boolean; variant?: 'default' | 'destructive'; }) { return ( <MenuPrimitive.Item data-slot="dropdown-menu-item" data-inset={inset} data-variant={variant} className={cn( "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0", className )} {...props} /> ); } function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) { return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />; } function DropdownMenuSubTrigger({ className, inset, children, ...props }: MenuPrimitive.SubmenuTrigger.Props & { inset?: boolean; }) { return ( <MenuPrimitive.SubmenuTrigger data-slot="dropdown-menu-sub-trigger" data-inset={inset} className={cn( "focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0", className )} {...props} > {children} <ChevronRightIcon className="ml-auto" /> </MenuPrimitive.SubmenuTrigger> ); } function DropdownMenuSubContent({ align = 'start', alignOffset = -3, side = 'right', sideOffset = 0, className, ...props }: React.ComponentProps<typeof DropdownMenuContent>) { return ( <DropdownMenuContent data-slot="dropdown-menu-sub-content" className={cn( 'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-24 rounded-md p-1 shadow-lg ring-1 duration-100 w-auto', className )} align={align} alignOffset={alignOffset} side={side} sideOffset={sideOffset} {...props} /> ); } function DropdownMenuCheckboxItem({ className, children, checked, ...props }: MenuPrimitive.CheckboxItem.Props) { return ( <MenuPrimitive.CheckboxItem data-slot="dropdown-menu-checkbox-item" className={cn( "focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", className )} checked={checked} {...props} > <span className="absolute right-2 flex items-center justify-center pointer-events-none" data-slot="dropdown-menu-checkbox-item-indicator" > <MenuPrimitive.CheckboxItemIndicator> <CheckIcon /> </MenuPrimitive.CheckboxItemIndicator> </span> {children} </MenuPrimitive.CheckboxItem> ); } function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) { return ( <MenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} /> ); } function DropdownMenuRadioItem({ className, children, ...props }: MenuPrimitive.RadioItem.Props) { return ( <MenuPrimitive.RadioItem data-slot="dropdown-menu-radio-item" className={cn( "focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", className )} {...props} > <span className="absolute right-2 flex items-center justify-center pointer-events-none" data-slot="dropdown-menu-radio-item-indicator" > <MenuPrimitive.RadioItemIndicator> <CheckIcon /> </MenuPrimitive.RadioItemIndicator> </span> {children} </MenuPrimitive.RadioItem> ); } function DropdownMenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props) { return ( <MenuPrimitive.Separator data-slot="dropdown-menu-separator" className={cn('bg-border -mx-1 my-1 h-px', className)} {...props} /> ); } function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) { return ( <span data-slot="dropdown-menu-shortcut" className={cn( 'text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest', className )} {...props} /> ); } export { DropdownMenu, DropdownMenuPortal, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent }; ================================================ FILE: packages/evershop/src/components/common/ui/Empty.tsx ================================================ import { cn } from '@evershop/evershop/lib/util/cn'; import { cva, type VariantProps } from 'class-variance-authority'; import React from 'react'; function Empty({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="empty" className={cn( 'gap-4 rounded-lg border-dashed p-12 flex w-full min-w-0 flex-1 flex-col items-center justify-center text-center text-balance', className )} {...props} /> ); } function EmptyHeader({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="empty-header" className={cn('gap-2 flex max-w-sm flex-col items-center', className)} {...props} /> ); } const emptyMediaVariants = cva( 'mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0', { variants: { variant: { default: 'bg-transparent', icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6" } }, defaultVariants: { variant: 'default' } } ); function EmptyMedia({ className, variant = 'default', ...props }: React.ComponentProps<'div'> & VariantProps<typeof emptyMediaVariants>) { return ( <div data-slot="empty-icon" data-variant={variant} className={cn(emptyMediaVariants({ variant, className }))} {...props} /> ); } function EmptyTitle({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="empty-title" className={cn('text-lg font-medium tracking-tight', className)} {...props} /> ); } function EmptyDescription({ className, ...props }: React.ComponentProps<'p'>) { return ( <div data-slot="empty-description" className={cn( 'text-sm/relaxed text-muted-foreground [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4', className )} {...props} /> ); } function EmptyContent({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="empty-content" className={cn( 'gap-4 text-sm flex w-full max-w-sm min-w-0 flex-col items-center text-balance', className )} {...props} /> ); } export { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent, EmptyMedia }; ================================================ FILE: packages/evershop/src/components/common/ui/Field.tsx ================================================ import { Label } from '@components/common/ui/Label.js'; import { Separator } from '@components/common/ui/Separator.js'; import { cn } from '@evershop/evershop/lib/util/cn'; import { cva, type VariantProps } from 'class-variance-authority'; import React, { useMemo } from 'react'; function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) { return ( <fieldset data-slot="field-set" className={cn( 'gap-6 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3 flex flex-col', className )} {...props} /> ); } function FieldLegend({ className, variant = 'legend', ...props }: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) { return ( <legend data-slot="field-legend" data-variant={variant} className={cn( 'mb-3 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base', className )} {...props} /> ); } function FieldGroup({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="field-group" className={cn( 'gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4 group/field-group @container/field-group flex w-full flex-col', className )} {...props} /> ); } const fieldVariants = cva( 'data-[invalid=true]:text-destructive gap-3 group/field flex w-full', { variants: { orientation: { vertical: 'flex-col [&>*]:w-full [&>.sr-only]:w-auto', horizontal: 'flex-row items-center [&>[data-slot=field-label]]:flex-auto has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px', responsive: 'flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto @md/field-group:[&>[data-slot=field-label]]:flex-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px' } }, defaultVariants: { orientation: 'vertical' } } ); function Field({ className, orientation = 'vertical', ...props }: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) { return ( <div role="group" data-slot="field" data-orientation={orientation} className={cn(fieldVariants({ orientation }), className)} {...props} /> ); } function FieldContent({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="field-content" className={cn( 'gap-1 group/field-content flex flex-1 flex-col leading-snug', className )} {...props} /> ); } function FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>) { return ( <Label data-slot="field-label" className={cn( 'has-data-checked:bg-primary/5 has-data-checked:border-primary dark:has-data-checked:bg-primary/10 gap-1 group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-3 group/field-label peer/field-label flex w-fit leading-snug', 'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col', className )} {...props} /> ); } function FieldTitle({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="field-label" className={cn( 'gap-2 text-sm font-medium group-data-[disabled=true]/field:opacity-50 flex w-fit items-center leading-snug', className )} {...props} /> ); } function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) { return ( <p data-slot="field-description" className={cn( 'text-muted-foreground text-left text-sm [[data-variant=legend]+&]:-mt-1.5 leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance', 'last:mt-0 nth-last-2:-mt-1', '[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4', className )} {...props} /> ); } function FieldSeparator({ children, className, ...props }: React.ComponentProps<'div'> & { children?: React.ReactNode; }) { return ( <div data-slot="field-separator" data-content={!!children} className={cn( '-my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2 relative', className )} {...props} > <Separator className="absolute inset-0 top-1/2" /> {children && ( <span className="text-muted-foreground px-2 bg-background relative mx-auto block w-fit" data-slot="field-separator-content" > {children} </span> )} </div> ); } function FieldError({ className, children, errors, ...props }: React.ComponentProps<'div'> & { errors?: Array<{ message?: string } | undefined>; }) { const content = useMemo(() => { if (children) { return children; } if (!errors?.length) { return null; } const uniqueErrors = [ ...new Map(errors.map((error) => [error?.message, error])).values() ]; if (uniqueErrors?.length == 1) { return uniqueErrors[0]?.message; } return ( <ul className="ml-4 flex list-disc flex-col gap-1"> {uniqueErrors.map( (error, index) => error?.message && <li key={index}>{error.message}</li> )} </ul> ); }, [children, errors]); if (!content) { return null; } return ( <div role="alert" data-slot="field-error" className={cn('text-destructive text-sm font-normal', className)} {...props} > {content} </div> ); } export { Field, FieldLabel, FieldDescription, FieldError, FieldGroup, FieldLegend, FieldSeparator, FieldSet, FieldContent, FieldTitle }; ================================================ FILE: packages/evershop/src/components/common/ui/HoverCard.tsx ================================================ import { PreviewCard as PreviewCardPrimitive } from '@base-ui/react/preview-card'; import { cn } from '@evershop/evershop/lib/util/cn'; import React from 'react'; function HoverCard({ ...props }: PreviewCardPrimitive.Root.Props) { return <PreviewCardPrimitive.Root data-slot="hover-card" {...props} />; } function HoverCardTrigger({ ...props }: PreviewCardPrimitive.Trigger.Props) { return ( <PreviewCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} /> ); } function HoverCardContent({ className, side = 'bottom', sideOffset = 4, align = 'center', alignOffset = 4, ...props }: PreviewCardPrimitive.Popup.Props & Pick< PreviewCardPrimitive.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset' >) { return ( <PreviewCardPrimitive.Portal data-slot="hover-card-portal"> <PreviewCardPrimitive.Positioner align={align} alignOffset={alignOffset} side={side} sideOffset={sideOffset} className="isolate z-50" > <PreviewCardPrimitive.Popup data-slot="hover-card-content" className={cn( 'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground w-64 rounded-lg p-4 text-sm shadow-md ring-1 duration-100 z-50 origin-(--transform-origin) outline-hidden', className )} {...props} /> </PreviewCardPrimitive.Positioner> </PreviewCardPrimitive.Portal> ); } export { HoverCard, HoverCardTrigger, HoverCardContent }; ================================================ FILE: packages/evershop/src/components/common/ui/Input.tsx ================================================ import { Input as InputPrimitive } from '@base-ui/react/input'; import { cn } from '@evershop/evershop/lib/util/cn'; import * as React from 'react'; const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>( ({ className, type, ...props }, ref) => { return ( <InputPrimitive ref={ref} type={type} data-slot="input" className={cn( 'dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-md border bg-transparent px-2.5 py-1 text-base shadow-xs transition-[color,box-shadow] file:h-7 file:text-sm file:font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50', className )} {...props} /> ); } ); Input.displayName = 'Input'; export { Input }; ================================================ FILE: packages/evershop/src/components/common/ui/InputGroup.tsx ================================================ /* eslint-disable jsx-a11y/click-events-have-key-events */ import { Button } from '@components/common/ui/Button.js'; import { Input } from '@components/common/ui/Input.js'; import { Textarea } from '@components/common/ui/Textarea.js'; import { cn } from '@evershop/evershop/lib/util/cn'; import { cva, type VariantProps } from 'class-variance-authority'; import * as React from 'react'; const InputGroup = React.forwardRef< HTMLDivElement, React.ComponentProps<'div'> >(({ className, ...props }, ref) => { return ( <div ref={ref} data-slot="input-group" role="group" className={cn( 'border-input dark:bg-input/30 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 h-9 rounded-md border shadow-xs transition-[color,box-shadow] has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot][aria-invalid=true]]:ring-[3px] has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5 [[data-slot=combobox-content]_&]:focus-within:border-inherit [[data-slot=combobox-content]_&]:focus-within:ring-0 group/input-group relative flex w-full min-w-0 items-center outline-none has-[>textarea]:h-auto', className )} {...props} /> ); }); InputGroup.displayName = 'InputGroup'; const inputGroupAddonVariants = cva( "text-muted-foreground h-auto gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4 flex cursor-text items-center justify-center select-none", { variants: { align: { 'inline-start': 'pl-2 has-[>button]:ml-[-0.25rem] has-[>kbd]:ml-[-0.15rem] order-first', 'inline-end': 'pr-2 has-[>button]:mr-[-0.25rem] has-[>kbd]:mr-[-0.15rem] order-last', 'block-start': 'px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2 order-first w-full justify-start', 'block-end': 'px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2 order-last w-full justify-start' } }, defaultVariants: { align: 'inline-start' } } ); const InputGroupAddon = React.forwardRef< HTMLDivElement, React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants> >(({ className, align = 'inline-start', ...props }, ref) => { return ( <div ref={ref} role="group" data-slot="input-group-addon" data-align={align} className={cn(inputGroupAddonVariants({ align }), className)} onClick={(e) => { if ((e.target as HTMLElement).closest('button')) { return; } e.currentTarget.parentElement?.querySelector('input')?.focus(); }} {...props} /> ); }); InputGroupAddon.displayName = 'InputGroupAddon'; const inputGroupButtonVariants = cva( 'gap-2 text-sm shadow-none flex items-center', { variants: { size: { xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5", sm: '', 'icon-xs': 'size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0', 'icon-sm': 'size-8 p-0 has-[>svg]:p-0' } }, defaultVariants: { size: 'xs' } } ); const InputGroupButton = React.forwardRef< HTMLButtonElement, Omit<React.ComponentProps<typeof Button>, 'size' | 'type'> & VariantProps<typeof inputGroupButtonVariants> & { type?: 'button' | 'submit' | 'reset'; } >(({ className, type = 'button', variant = 'ghost', size = 'xs', ...props }, ref) => { return ( <Button ref={ref} type={type} data-size={size} variant={variant} className={cn(inputGroupButtonVariants({ size }), className)} {...props} /> ); }); InputGroupButton.displayName = 'InputGroupButton'; const InputGroupText = React.forwardRef< HTMLSpanElement, React.ComponentProps<'span'> >(({ className, ...props }, ref) => { return ( <span ref={ref} className={cn( "text-muted-foreground gap-2 text-sm [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none", className )} {...props} /> ); }); InputGroupText.displayName = 'InputGroupText'; const InputGroupInput = React.forwardRef< HTMLInputElement, React.ComponentProps<'input'> >(({ className, ...props }, ref) => { return ( <Input ref={ref} data-slot="input-group-control" className={cn( 'rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent flex-1', className )} {...props} /> ); }); InputGroupInput.displayName = 'InputGroupInput'; const InputGroupTextarea = React.forwardRef< HTMLTextAreaElement, React.ComponentProps<'textarea'> >(({ className, ...props }, ref) => { return ( <Textarea ref={ref} data-slot="input-group-control" className={cn( 'rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent flex-1 resize-none', className )} {...props} /> ); }); InputGroupTextarea.displayName = 'InputGroupTextarea'; export { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea }; ================================================ FILE: packages/evershop/src/components/common/ui/Item.tsx ================================================ import { mergeProps } from '@base-ui/react/merge-props'; import { useRender } from '@base-ui/react/use-render'; import { Separator } from '@components/common/ui/Separator.js'; import { cn } from '@evershop/evershop/lib/util/cn'; import { cva, type VariantProps } from 'class-variance-authority'; import * as React from 'react'; function ItemGroup({ className, ...props }: React.ComponentProps<'div'>) { return ( <div role="list" data-slot="item-group" className={cn( 'gap-4 has-[[data-size=sm]]:gap-2.5 has-[[data-size=xs]]:gap-2 group/item-group flex w-full flex-col', className )} {...props} /> ); } function ItemSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) { return ( <Separator data-slot="item-separator" orientation="horizontal" className={cn('my-2', className)} {...props} /> ); } const itemVariants = cva( '[a]:hover:bg-muted rounded-md border text-sm w-full group/item focus-visible:border-ring focus-visible:ring-ring/50 flex items-center flex-wrap outline-none transition-colors duration-100 focus-visible:ring-[3px] [a]:transition-colors', { variants: { variant: { default: 'border-transparent', outline: 'border-border', muted: 'bg-muted/50 border-transparent' }, size: { default: 'gap-3.5 px-4 py-3.5', sm: 'gap-2.5 px-3 py-2.5', xs: 'gap-2 px-2.5 py-2 [[data-slot=dropdown-menu-content]_&]:p-0' } }, defaultVariants: { variant: 'default', size: 'default' } } ); function Item({ className, variant = 'default', size = 'default', render, ...props }: useRender.ComponentProps<'div'> & VariantProps<typeof itemVariants>) { return useRender({ defaultTagName: 'div', props: mergeProps<'div'>( { className: cn(itemVariants({ variant, size, className })) }, props ), render, state: { slot: 'item', variant, size } }); } const itemMediaVariants = cva( 'gap-2 group-has-[[data-slot=item-description]]/item:translate-y-0.5 group-has-[[data-slot=item-description]]/item:self-start flex shrink-0 items-center justify-center [&_svg]:pointer-events-none', { variants: { variant: { default: 'bg-transparent', icon: "[&_svg:not([class*='size-'])]:size-4", image: 'size-10 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover' } }, defaultVariants: { variant: 'default' } } ); function ItemMedia({ className, variant = 'default', ...props }: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>) { return ( <div data-slot="item-media" data-variant={variant} className={cn(itemMediaVariants({ variant, className }))} {...props} /> ); } function ItemContent({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="item-content" className={cn( 'gap-1 group-data-[size=xs]/item:gap-0 flex flex-1 flex-col [&+[data-slot=item-content]]:flex-none', className )} {...props} /> ); } function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="item-title" className={cn( 'gap-2 text-sm leading-snug font-medium underline-offset-4 line-clamp-1 flex w-fit items-center', className )} {...props} /> ); } function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) { return ( <p data-slot="item-description" className={cn( 'text-muted-foreground text-left text-sm leading-normal group-data-[size=xs]/item:text-xs [&>a:hover]:text-primary line-clamp-2 font-normal [&>a]:underline [&>a]:underline-offset-4', className )} {...props} /> ); } function ItemActions({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="item-actions" className={cn('gap-2 flex items-center', className)} {...props} /> ); } function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="item-header" className={cn( 'gap-2 flex basis-full items-center justify-between', className )} {...props} /> ); } function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="item-footer" className={cn( 'gap-2 flex basis-full items-center justify-between', className )} {...props} /> ); } export { Item, ItemMedia, ItemContent, ItemActions, ItemGroup, ItemSeparator, ItemTitle, ItemDescription, ItemHeader, ItemFooter }; ================================================ FILE: packages/evershop/src/components/common/ui/Kbd.tsx ================================================ import { cn } from '@evershop/evershop/lib/util/cn'; import React from 'react'; function Kbd({ className, ...props }: React.ComponentProps<'kbd'>) { return ( <kbd data-slot="kbd" className={cn( "bg-muted text-muted-foreground [[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10 h-5 w-fit min-w-5 gap-1 rounded-sm px-1 font-sans text-xs font-medium [&_svg:not([class*='size-'])]:size-3 pointer-events-none inline-flex items-center justify-center select-none", className )} {...props} /> ); } function KbdGroup({ className, ...props }: React.ComponentProps<'div'>) { return ( <kbd data-slot="kbd-group" className={cn('gap-1 inline-flex items-center', className)} {...props} /> ); } export { Kbd, KbdGroup }; ================================================ FILE: packages/evershop/src/components/common/ui/Label.tsx ================================================ import { cn } from '@evershop/evershop/lib/util/cn'; import * as React from 'react'; function Label({ className, ...props }: React.ComponentProps<'label'>) { return ( <label data-slot="label" className={cn( 'gap-2 text-sm leading-none font-medium group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed', className )} {...props} /> ); } export { Label }; ================================================ FILE: packages/evershop/src/components/common/ui/Menubar.tsx ================================================ import { Menu as MenuPrimitive } from '@base-ui/react/menu'; import { Menubar as MenubarPrimitive } from '@base-ui/react/menubar'; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '@components/common/ui/DropdownMenu.js'; import { cn } from '@evershop/evershop/lib/util/cn'; import { CheckIcon } from 'lucide-react'; import * as React from 'react'; function Menubar({ className, ...props }: MenubarPrimitive.Props) { return ( <MenubarPrimitive data-slot="menubar" className={cn( 'bg-background h-9 gap-1 rounded-md border p-1 shadow-xs flex items-center', className )} {...props} /> ); } function MenubarMenu({ ...props }: React.ComponentProps<typeof DropdownMenu>) { return <DropdownMenu data-slot="menubar-menu" {...props} />; } function MenubarGroup({ ...props }: React.ComponentProps<typeof DropdownMenuGroup>) { return <DropdownMenuGroup data-slot="menubar-group" {...props} />; } function MenubarPortal({ ...props }: React.ComponentProps<typeof DropdownMenuPortal>) { return <DropdownMenuPortal data-slot="menubar-portal" {...props} />; } function MenubarTrigger({ className, ...props }: React.ComponentProps<typeof DropdownMenuTrigger>) { return ( <DropdownMenuTrigger data-slot="menubar-trigger" className={cn( 'hover:bg-muted aria-expanded:bg-muted rounded-sm px-2 py-1 text-sm font-medium flex items-center outline-hidden select-none', className )} {...props} /> ); } function MenubarContent({ className, align = 'start', alignOffset = -4, sideOffset = 8, ...props }: React.ComponentProps<typeof DropdownMenuContent>) { return ( <DropdownMenuContent data-slot="menubar-content" align={align} alignOffset={alignOffset} sideOffset={sideOffset} className={cn( 'bg-popover text-popover-foreground data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-36 rounded-md p-1 shadow-md ring-1 duration-100', className )} {...props} /> ); } function MenubarItem({ className, inset, variant = 'default', ...props }: React.ComponentProps<typeof DropdownMenuItem>) { return ( <DropdownMenuItem data-slot="menubar-item" data-inset={inset} data-variant={variant} className={cn( "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg:not([class*='size-'])]:size-4 group/menubar-item", className )} {...props} /> ); } function MenubarCheckboxItem({ className, children, checked, ...props }: MenuPrimitive.CheckboxItem.Props) { return ( <MenuPrimitive.CheckboxItem data-slot="menubar-checkbox-item" className={cn( 'focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-md py-1.5 pr-2 pl-8 text-sm data-disabled:opacity-50 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0', className )} checked={checked} {...props} > <span className="left-2 size-4 [&_svg:not([class*='size-'])]:size-4 pointer-events-none absolute flex items-center justify-center"> <MenuPrimitive.CheckboxItemIndicator> <CheckIcon /> </MenuPrimitive.CheckboxItemIndicator> </span> {children} </MenuPrimitive.CheckboxItem> ); } function MenubarRadioGroup({ ...props }: React.ComponentProps<typeof DropdownMenuRadioGroup>) { return <DropdownMenuRadioGroup data-slot="menubar-radio-group" {...props} />; } function MenubarRadioItem({ className, children, ...props }: MenuPrimitive.RadioItem.Props) { return ( <MenuPrimitive.RadioItem data-slot="menubar-radio-item" className={cn( "focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-md py-1.5 pr-2 pl-8 text-sm data-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0", className )} {...props} > <span className="left-2 size-4 [&_svg:not([class*='size-'])]:size-4 pointer-events-none absolute flex items-center justify-center"> <MenuPrimitive.RadioItemIndicator> <CheckIcon /> </MenuPrimitive.RadioItemIndicator> </span> {children} </MenuPrimitive.RadioItem> ); } function MenubarLabel({ className, inset, ...props }: React.ComponentProps<typeof DropdownMenuLabel>) { return ( <DropdownMenuLabel data-slot="menubar-label" data-inset={inset} className={cn( 'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', className )} {...props} /> ); } function MenubarSeparator({ className, ...props }: React.ComponentProps<typeof DropdownMenuSeparator>) { return ( <DropdownMenuSeparator data-slot="menubar-separator" className={cn('bg-border -mx-1 my-1 h-px', className)} {...props} /> ); } function MenubarShortcut({ className, ...props }: React.ComponentProps<typeof DropdownMenuShortcut>) { return ( <DropdownMenuShortcut data-slot="menubar-shortcut" className={cn( 'text-muted-foreground group-focus/menubar-item:text-accent-foreground text-xs tracking-widest ml-auto', className )} {...props} /> ); } function MenubarSub({ ...props }: React.ComponentProps<typeof DropdownMenuSub>) { return <DropdownMenuSub data-slot="menubar-sub" {...props} />; } function MenubarSubTrigger({ className, inset, ...props }: React.ComponentProps<typeof DropdownMenuSubTrigger> & { inset?: boolean; }) { return ( <DropdownMenuSubTrigger data-slot="menubar-sub-trigger" data-inset={inset} className={cn( "focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-[inset]:pl-8 [&_svg:not([class*='size-'])]:size-4", className )} {...props} /> ); } function MenubarSubContent({ className, ...props }: React.ComponentProps<typeof DropdownMenuSubContent>) { return ( <DropdownMenuSubContent data-slot="menubar-sub-content" className={cn( 'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-32 rounded-md p-1 shadow-lg ring-1 duration-100', className )} {...props} /> ); } export { Menubar, MenubarPortal, MenubarMenu, MenubarTrigger, MenubarContent, MenubarGroup, MenubarSeparator, MenubarLabel, MenubarItem, MenubarShortcut, MenubarCheckboxItem, MenubarRadioGroup, MenubarRadioItem, MenubarSub, MenubarSubTrigger, MenubarSubContent }; ================================================ FILE: packages/evershop/src/components/common/ui/NavigationMenu.tsx ================================================ import { NavigationMenu as NavigationMenuPrimitive } from '@base-ui/react/navigation-menu'; import { cn } from '@evershop/evershop/lib/util/cn'; import { cva } from 'class-variance-authority'; import { ChevronDownIcon } from 'lucide-react'; import React from 'react'; function NavigationMenu({ className, children, ...props }: NavigationMenuPrimitive.Root.Props) { return ( <NavigationMenuPrimitive.Root data-slot="navigation-menu" className={cn( 'group/navigation-menu relative flex max-w-max flex-1 items-center justify-center', className )} {...props} > {children} <NavigationMenuPositioner /> </NavigationMenuPrimitive.Root> ); } function NavigationMenuList({ className, ...props }: NavigationMenuPrimitive.List.Props) { return ( <NavigationMenuPrimitive.List data-slot="navigation-menu-list" className={cn( 'gap-0 group flex flex-1 list-none items-center justify-center', className )} {...(props as any)} /> ); } function NavigationMenuItem({ className, ...props }: NavigationMenuPrimitive.Item.Props) { return ( <NavigationMenuPrimitive.Item data-slot="navigation-menu-item" className={cn('relative', className)} {...(props as any)} /> ); } const navigationMenuTriggerStyle = cva( 'bg-background hover:bg-muted focus:bg-muted data-open:hover:bg-muted data-open:focus:bg-muted data-open:bg-muted/50 focus-visible:ring-ring/50 data-popup-open:bg-muted/50 data-popup-open:hover:bg-muted rounded-md px-4 py-2 text-sm font-medium transition-all focus-visible:ring-[3px] focus-visible:outline-1 disabled:opacity-50 group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center disabled:pointer-events-none outline-none' ); function NavigationMenuTrigger({ className, children, ...props }: NavigationMenuPrimitive.Trigger.Props) { return ( <NavigationMenuPrimitive.Trigger data-slot="navigation-menu-trigger" className={cn(navigationMenuTriggerStyle(), 'group', className)} {...props} > {children}{' '} <ChevronDownIcon className="relative top-px ml-1 size-3 transition duration-300 group-data-open/navigation-menu-trigger:rotate-180 group-data-popup-open/navigation-menu-trigger:rotate-180" aria-hidden="true" /> </NavigationMenuPrimitive.Trigger> ); } function NavigationMenuContent({ className, ...props }: NavigationMenuPrimitive.Content.Props) { return ( <NavigationMenuPrimitive.Content data-slot="navigation-menu-content" className={cn( 'data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-open:animate-in group-data-[viewport=false]/navigation-menu:data-closed:animate-out group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 group-data-[viewport=false]/navigation-menu:ring-foreground/10 p-2 pr-2.5 ease-[cubic-bezier(0.22,1,0.36,1)] group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:ring-1 group-data-[viewport=false]/navigation-menu:duration-300 h-full w-auto **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none', className )} {...props} /> ); } function NavigationMenuPositioner({ className, side = 'bottom', sideOffset = 8, align = 'start', alignOffset = 0, ...props }: NavigationMenuPrimitive.Positioner.Props) { return ( <NavigationMenuPrimitive.Portal> <NavigationMenuPrimitive.Positioner side={side} sideOffset={sideOffset} align={align} alignOffset={alignOffset} className={cn( 'transition-[top,left,right,bottom] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] data-[side=bottom]:before:-top-2.5 data-[side=bottom]:before:right-0 data-[side=bottom]:before:left-0 isolate z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) data-instant:transition-none', className )} {...props} > <NavigationMenuPrimitive.Popup className="bg-popover text-popover-foreground ring-foreground/10 rounded-lg shadow ring-1 transition-all ease-[cubic-bezier(0.22,1,0.36,1)] outline-none data-ending-style:scale-90 data-ending-style:opacity-0 data-ending-style:duration-150 data-starting-style:scale-90 data-starting-style:opacity-0 xs:w-(--popup-width) relative h-(--popup-height) w-(--popup-width) origin-(--transform-origin)"> <NavigationMenuPrimitive.Viewport className="relative size-full overflow-hidden" /> </NavigationMenuPrimitive.Popup> </NavigationMenuPrimitive.Positioner> </NavigationMenuPrimitive.Portal> ); } function NavigationMenuLink({ className, ...props }: NavigationMenuPrimitive.Link.Props) { return ( <NavigationMenuPrimitive.Link data-slot="navigation-menu-link" className={cn( "data-[active=true]:focus:bg-muted data-[active=true]:hover:bg-muted data-[active=true]:bg-muted/50 focus-visible:ring-ring/50 hover:bg-muted focus:bg-muted flex items-center gap-1.5 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4", className )} {...props} /> ); } function NavigationMenuIndicator({ className, ...props }: NavigationMenuPrimitive.Icon.Props) { return ( <NavigationMenuPrimitive.Icon data-slot="navigation-menu-indicator" className={cn( 'data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-1 flex h-1.5 items-end justify-center overflow-hidden', className )} {...(props as any)} > <div className="bg-border rounded-tl-sm shadow-md relative top-[60%] h-2 w-2 rotate-45" /> </NavigationMenuPrimitive.Icon> ); } export { NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, navigationMenuTriggerStyle, NavigationMenuPositioner }; ================================================ FILE: packages/evershop/src/components/common/ui/Pagination.tsx ================================================ /* eslint-disable jsx-a11y/anchor-has-content */ import { Button } from '@components/common/ui/Button.js'; import { cn } from '@evershop/evershop/lib/util/cn'; import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react'; import * as React from 'react'; function Pagination({ className, ...props }: React.ComponentProps<'nav'>) { return ( <nav role="navigation" aria-label="pagination" data-slot="pagination" className={cn('mx-auto flex w-full justify-center', className)} {...props} /> ); } function PaginationContent({ className, ...props }: React.ComponentProps<'ul'>) { return ( <ul data-slot="pagination-content" className={cn('gap-1 flex items-center', className)} {...props} /> ); } function PaginationItem({ ...props }: React.ComponentProps<'li'>) { return <li data-slot="pagination-item" {...props} />; } type PaginationLinkProps = { isActive?: boolean; } & Pick<React.ComponentProps<typeof Button>, 'size'> & React.ComponentProps<'a'>; function PaginationLink({ className, isActive, size = 'icon', ...props }: PaginationLinkProps) { return ( <Button variant={isActive ? 'outline' : 'ghost'} size={size} className={cn(className)} nativeButton={false} render={ <a aria-current={isActive ? 'page' : undefined} data-slot="pagination-link" data-active={isActive} {...props} /> } /> ); } function PaginationPrevious({ className, ...props }: React.ComponentProps<typeof PaginationLink>) { return ( <PaginationLink aria-label="Go to previous page" size="default" className={cn('pl-2!', className)} {...props} > <ChevronLeftIcon data-icon="inline-start" /> <span className="hidden sm:block">Previous</span> </PaginationLink> ); } function PaginationNext({ className, ...props }: React.ComponentProps<typeof PaginationLink>) { return ( <PaginationLink aria-label="Go to next page" size="default" className={cn('pr-2!', className)} {...props} > <span className="hidden sm:block">Next</span> <ChevronRightIcon data-icon="inline-end" /> </PaginationLink> ); } function PaginationEllipsis({ className, ...props }: React.ComponentProps<'span'>) { return ( <span aria-hidden data-slot="pagination-ellipsis" className={cn( "size-9 items-center justify-center [&_svg:not([class*='size-'])]:size-4 flex items-center justify-center", className )} {...props} > <MoreHorizontalIcon /> <span className="sr-only">More pages</span> </span> ); } export { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious }; ================================================ FILE: packages/evershop/src/components/common/ui/Popover.tsx ================================================ import { Popover as PopoverPrimitive } from '@base-ui/react/popover'; import { cn } from '@evershop/evershop/lib/util/cn'; import * as React from 'react'; function Popover({ ...props }: PopoverPrimitive.Root.Props) { return <PopoverPrimitive.Root data-slot="popover" {...props} />; } function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) { return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />; } function PopoverContent({ className, align = 'center', alignOffset = 0, side = 'bottom', sideOffset = 4, ...props }: PopoverPrimitive.Popup.Props & Pick< PopoverPrimitive.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset' >) { return ( <PopoverPrimitive.Portal> <PopoverPrimitive.Positioner align={align} alignOffset={alignOffset} side={side} sideOffset={sideOffset} className="isolate z-50" > <PopoverPrimitive.Popup data-slot="popover-content" className={cn( 'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 flex flex-col gap-4 rounded-md p-4 text-sm shadow-md ring-1 duration-100 z-50 w-72 origin-(--transform-origin) outline-hidden', className )} {...props} /> </PopoverPrimitive.Positioner> </PopoverPrimitive.Portal> ); } function PopoverHeader({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="popover-header" className={cn('flex flex-col gap-1 text-sm', className)} {...props} /> ); } function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) { return ( <PopoverPrimitive.Title data-slot="popover-title" className={cn('font-medium', className)} {...props} /> ); } function PopoverDescription({ className, ...props }: PopoverPrimitive.Description.Props) { return ( <PopoverPrimitive.Description data-slot="popover-description" className={cn('text-muted-foreground', className)} {...props} /> ); } export { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger }; ================================================ FILE: packages/evershop/src/components/common/ui/Progress.tsx ================================================ import { Progress as ProgressPrimitive } from '@base-ui/react/progress'; import { cn } from '@evershop/evershop/lib/util/cn'; import React from 'react'; function Progress({ className, children, value, ...props }: ProgressPrimitive.Root.Props) { return ( <ProgressPrimitive.Root value={value} data-slot="progress" className={cn('flex flex-wrap gap-3', className)} {...props} > {children} <ProgressTrack> <ProgressIndicator /> </ProgressTrack> </ProgressPrimitive.Root> ); } function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) { return ( <ProgressPrimitive.Track className={cn( 'bg-muted h-1.5 rounded-full relative flex w-full items-center overflow-x-hidden', className )} data-slot="progress-track" {...props} /> ); } function ProgressIndicator({ className, ...props }: ProgressPrimitive.Indicator.Props) { return ( <ProgressPrimitive.Indicator data-slot="progress-indicator" className={cn('bg-primary h-full transition-all', className)} {...props} /> ); } function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) { return ( <ProgressPrimitive.Label className={cn('text-sm font-medium', className)} data-slot="progress-label" {...props} /> ); } function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) { return ( <ProgressPrimitive.Value className={cn( 'text-muted-foreground ml-auto text-sm tabular-nums', className )} data-slot="progress-value" {...props} /> ); } export { Progress, ProgressTrack, ProgressIndicator, ProgressLabel, ProgressValue }; ================================================ FILE: packages/evershop/src/components/common/ui/RadioGroup.tsx ================================================ import { Radio as RadioPrimitive } from '@base-ui/react/radio'; import { RadioGroup as RadioGroupPrimitive } from '@base-ui/react/radio-group'; import { cn } from '@evershop/evershop/lib/util/cn'; import { CircleIcon } from 'lucide-react'; import React from 'react'; function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) { return ( <RadioGroupPrimitive data-slot="radio-group" className={cn('grid gap-3 w-full', className)} {...props} /> ); } function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) { return ( <RadioPrimitive.Root data-slot="radio-group-item" className={cn( 'border-input text-primary dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 flex size-4 rounded-full shadow-xs focus-visible:ring-[3px] aria-invalid:ring-[3px] group/radio-group-item peer relative aspect-square shrink-0 border outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50', className )} {...props} > <RadioPrimitive.Indicator data-slot="radio-group-indicator" className="group-aria-invalid/radio-group-item:text-destructive text-primary flex size-4 items-center justify-center" > <CircleIcon className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 fill-current" /> </RadioPrimitive.Indicator> </RadioPrimitive.Root> ); } export { RadioGroup, RadioGroupItem }; ================================================ FILE: packages/evershop/src/components/common/ui/ScrollArea.tsx ================================================ import { ScrollArea as ScrollAreaPrimitive } from '@base-ui/react/scroll-area'; import { cn } from '@evershop/evershop/lib/util/cn'; import * as React from 'react'; function ScrollArea({ className, children, ...props }: ScrollAreaPrimitive.Root.Props) { return ( <ScrollAreaPrimitive.Root data-slot="scroll-area" className={cn('relative', className)} {...props} > <ScrollAreaPrimitive.Viewport data-slot="scroll-area-viewport" className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1" > {children} </ScrollAreaPrimitive.Viewport> <ScrollBar /> <ScrollAreaPrimitive.Corner /> </ScrollAreaPrimitive.Root> ); } function ScrollBar({ className, orientation = 'vertical', ...props }: ScrollAreaPrimitive.Scrollbar.Props) { return ( <ScrollAreaPrimitive.Scrollbar data-slot="scroll-area-scrollbar" data-orientation={orientation} orientation={orientation} className={cn( 'data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent flex touch-none p-px transition-colors select-none', className )} {...props} > <ScrollAreaPrimitive.Thumb data-slot="scroll-area-thumb" className="rounded-full bg-border relative flex-1" /> </ScrollAreaPrimitive.Scrollbar> ); } export { ScrollArea, ScrollBar }; ================================================ FILE: packages/evershop/src/components/common/ui/Select.tsx ================================================ import { Select as SelectPrimitive } from '@base-ui/react/select'; import { cn } from '@evershop/evershop/lib/util/cn'; import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from 'lucide-react'; import * as React from 'react'; const Select = SelectPrimitive.Root; function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) { return ( <SelectPrimitive.Group data-slot="select-group" className={cn('scroll-my-1 p-1', className)} {...props} /> ); } function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) { return ( <SelectPrimitive.Value data-slot="select-value" className={cn('flex flex-1 text-left', className)} {...props} /> ); } function SelectTrigger({ className, size = 'default', children, ...props }: SelectPrimitive.Trigger.Props & { size?: 'sm' | 'default'; }) { return ( <SelectPrimitive.Trigger data-slot="select-trigger" data-size={size} className={cn( "border-input data-[placeholder]:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-md border bg-transparent py-2 pr-2 pl-2.5 text-sm shadow-xs transition-[color,box-shadow] focus-visible:ring-[3px] aria-invalid:ring-[3px] data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:flex *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-4 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0", className )} {...props} > {children} <SelectPrimitive.Icon render={ <ChevronDownIcon className="text-muted-foreground size-4 pointer-events-none" /> } /> </SelectPrimitive.Trigger> ); } function SelectContent({ className, children, side = 'bottom', sideOffset = 4, align = 'center', alignOffset = 0, alignItemWithTrigger = true, ...props }: SelectPrimitive.Popup.Props & Pick< SelectPrimitive.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset' | 'alignItemWithTrigger' >) { return ( <SelectPrimitive.Portal> <SelectPrimitive.Positioner side={side} sideOffset={sideOffset} align={align} alignOffset={alignOffset} alignItemWithTrigger={alignItemWithTrigger} className="isolate z-5000" > <SelectPrimitive.Popup data-slot="select-content" className={cn( 'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-36 rounded-md shadow-md ring-1 duration-100 relative isolate z-5000 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto', className )} {...props} > <SelectScrollUpButton /> <SelectPrimitive.List>{children}</SelectPrimitive.List> <SelectScrollDownButton /> </SelectPrimitive.Popup> </SelectPrimitive.Positioner> </SelectPrimitive.Portal> ); } function SelectLabel({ className, ...props }: SelectPrimitive.GroupLabel.Props) { return ( <SelectPrimitive.GroupLabel data-slot="select-label" className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)} {...props} /> ); } function SelectItem({ className, children, ...props }: SelectPrimitive.Item.Props) { return ( <SelectPrimitive.Item data-slot="select-item" className={cn( "focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", className )} {...props} > <SelectPrimitive.ItemText className="flex flex-1 gap-2 shrink-0 whitespace-nowrap"> {children} </SelectPrimitive.ItemText> <SelectPrimitive.ItemIndicator render={ <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" /> } > <CheckIcon className="pointer-events-none" /> </SelectPrimitive.ItemIndicator> </SelectPrimitive.Item> ); } function SelectSeparator({ className, ...props }: SelectPrimitive.Separator.Props) { return ( <SelectPrimitive.Separator data-slot="select-separator" className={cn('bg-border -mx-1 my-1 h-px pointer-events-none', className)} {...props} /> ); } function SelectScrollUpButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) { return ( <SelectPrimitive.ScrollUpArrow data-slot="select-scroll-up-button" className={cn( "bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 top-0 w-full", className )} {...props} > <ChevronUpIcon /> </SelectPrimitive.ScrollUpArrow> ); } function SelectScrollDownButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) { return ( <SelectPrimitive.ScrollDownArrow data-slot="select-scroll-down-button" className={cn( "bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 bottom-0 w-full", className )} {...props} > <ChevronDownIcon /> </SelectPrimitive.ScrollDownArrow> ); } export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue }; ================================================ FILE: packages/evershop/src/components/common/ui/Separator.tsx ================================================ import { Separator as SeparatorPrimitive } from '@base-ui/react/separator'; import { cn } from '@evershop/evershop/lib/util/cn'; import React from 'react'; function Separator({ className, orientation = 'horizontal', ...props }: SeparatorPrimitive.Props) { return ( <SeparatorPrimitive data-slot="separator" orientation={orientation} className={cn( 'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:self-stretch', className )} {...props} /> ); } export { Separator }; ================================================ FILE: packages/evershop/src/components/common/ui/Sheet.tsx ================================================ import { Dialog as SheetPrimitive } from '@base-ui/react/dialog'; import { Button } from '@components/common/ui/Button.js'; import { cn } from '@evershop/evershop/lib/util/cn'; import { XIcon } from 'lucide-react'; import * as React from 'react'; function Sheet({ ...props }: SheetPrimitive.Root.Props) { return <SheetPrimitive.Root data-slot="sheet" {...props} />; } function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) { return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />; } function SheetClose({ ...props }: SheetPrimitive.Close.Props) { return <SheetPrimitive.Close data-slot="sheet-close" {...props} />; } function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) { return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />; } function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) { return ( <SheetPrimitive.Backdrop data-slot="sheet-overlay" className={cn( 'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50', className )} {...props} /> ); } function SheetContent({ className, children, side = 'right', showCloseButton = true, ...props }: SheetPrimitive.Popup.Props & { side?: 'top' | 'right' | 'bottom' | 'left'; showCloseButton?: boolean; }) { return ( <SheetPortal> <SheetOverlay /> <SheetPrimitive.Popup data-slot="sheet-content" data-side={side} className={cn( 'bg-background data-open:animate-in data-closed:animate-out data-[side=right]:data-closed:slide-out-to-right-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=top]:data-closed:slide-out-to-top-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:fade-out-0 data-open:fade-in-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=bottom]:data-open:slide-in-from-bottom-10 fixed z-50 flex flex-col gap-4 bg-clip-padding text-sm shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm', className )} {...props} > {children} {showCloseButton && ( <SheetPrimitive.Close data-slot="sheet-close" render={ <Button variant="ghost" className="absolute top-4 right-4" size="icon-sm" /> } > <XIcon /> <span className="sr-only">Close</span> </SheetPrimitive.Close> )} </SheetPrimitive.Popup> </SheetPortal> ); } function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="sheet-header" className={cn('gap-1.5 p-4 flex flex-col', className)} {...props} /> ); } function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="sheet-footer" className={cn('gap-2 p-4 mt-auto flex flex-col', className)} {...props} /> ); } function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) { return ( <SheetPrimitive.Title data-slot="sheet-title" className={cn('text-foreground font-medium', className)} {...props} /> ); } function SheetDescription({ className, ...props }: SheetPrimitive.Description.Props) { return ( <SheetPrimitive.Description data-slot="sheet-description" className={cn('text-muted-foreground text-sm', className)} {...props} /> ); } export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription }; ================================================ FILE: packages/evershop/src/components/common/ui/Sidebar.tsx ================================================ import { mergeProps } from '@base-ui/react/merge-props'; import { useRender } from '@base-ui/react/use-render'; import { Button } from '@components/common/ui/Button.js'; import { useIsMobile } from '@components/common/ui/hooks/useIsMobile.js'; import { Input } from '@components/common/ui/Input.js'; import { Separator } from '@components/common/ui/Separator.js'; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@components/common/ui/Sheet.js'; import { Skeleton } from '@components/common/ui/Skeleton.js'; import { Tooltip, TooltipContent, TooltipTrigger } from '@components/common/ui/Tooltip.js'; import { cn } from '@evershop/evershop/lib/util/cn'; import { cva, type VariantProps } from 'class-variance-authority'; import { PanelLeftIcon } from 'lucide-react'; import * as React from 'react'; const SIDEBAR_COOKIE_NAME = 'sidebar_state'; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; const SIDEBAR_WIDTH = '16rem'; const SIDEBAR_WIDTH_MOBILE = '18rem'; const SIDEBAR_WIDTH_ICON = '3rem'; const SIDEBAR_KEYBOARD_SHORTCUT = 'b'; type SidebarContextProps = { state: 'expanded' | 'collapsed'; open: boolean; setOpen: (open: boolean) => void; openMobile: boolean; setOpenMobile: (open: boolean) => void; isMobile: boolean; toggleSidebar: () => void; }; const SidebarContext = React.createContext<SidebarContextProps | null>(null); function useSidebar() { const context = React.useContext(SidebarContext); if (!context) { throw new Error('useSidebar must be used within a SidebarProvider.'); } return context; } function SidebarProvider({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props }: React.ComponentProps<'div'> & { defaultOpen?: boolean; open?: boolean; onOpenChange?: (open: boolean) => void; }) { const isMobile = useIsMobile(); const [openMobile, setOpenMobile] = React.useState(false); // This is the internal state of the sidebar. // We use openProp and setOpenProp for control from outside the component. const [_open, _setOpen] = React.useState(defaultOpen); const open = openProp ?? _open; const setOpen = React.useCallback( (value: boolean | ((value: boolean) => boolean)) => { const openState = typeof value === 'function' ? value(open) : value; if (setOpenProp) { setOpenProp(openState); } else { _setOpen(openState); } // This sets the cookie to keep the sidebar state. document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; }, [setOpenProp, open] ); // Helper to toggle the sidebar. const toggleSidebar = React.useCallback(() => { return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open); }, [isMobile, setOpen, setOpenMobile]); // Adds a keyboard shortcut to toggle the sidebar. React.useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if ( event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey) ) { event.preventDefault(); toggleSidebar(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [toggleSidebar]); // We add a state so that we can do data-state="expanded" or "collapsed". // This makes it easier to style the sidebar with Tailwind classes. const state = open ? 'expanded' : 'collapsed'; const contextValue = React.useMemo<SidebarContextProps>( () => ({ state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar }), [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] ); return ( <SidebarContext.Provider value={contextValue}> <div data-slot="sidebar-wrapper" style={ { '--sidebar-width': SIDEBAR_WIDTH, '--sidebar-width-icon': SIDEBAR_WIDTH_ICON, ...style } as React.CSSProperties } className={cn( 'group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full', className )} {...props} > {children} </div> </SidebarContext.Provider> ); } function Sidebar({ side = 'left', variant = 'sidebar', collapsible = 'offExamples', className, children, ...props }: React.ComponentProps<'div'> & { side?: 'left' | 'right'; variant?: 'sidebar' | 'floating' | 'inset'; collapsible?: 'offExamples' | 'icon' | 'none'; }) { const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); if (collapsible === 'none') { return ( <div data-slot="sidebar" className={cn( 'bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col', className )} {...props} > {children} </div> ); } if (isMobile) { return ( <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}> <SheetContent data-sidebar="sidebar" data-slot="sidebar" data-mobile="true" className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden" style={ { '--sidebar-width': SIDEBAR_WIDTH_MOBILE } as React.CSSProperties } side={side} > <SheetHeader className="sr-only"> <SheetTitle>Sidebar</SheetTitle> <SheetDescription>Displays the mobile sidebar.</SheetDescription> </SheetHeader> <div className="flex h-full w-full flex-col">{children}</div> </SheetContent> </Sheet> ); } return ( <div className="group peer text-sidebar-foreground hidden md:block" data-state={state} data-collapsible={state === 'collapsed' ? collapsible : ''} data-variant={variant} data-side={side} data-slot="sidebar" > {/* This is what handles the sidebar gap on desktop */} <div data-slot="sidebar-gap" className={cn( 'transition-[width] duration-200 ease-linear relative w-(--sidebar-width) bg-transparent', 'group-data-[collapsible=offExamples]:w-0', 'group-data-[side=right]:rotate-180', variant === 'floating' || variant === 'inset' ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]' : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)' )} /> <div data-slot="sidebar-container" className={cn( 'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex', side === 'left' ? 'left-0 group-data-[collapsible=offExamples]:left-[calc(var(--sidebar-width)*-1)]' : 'right-0 group-data-[collapsible=offExamples]:right-[calc(var(--sidebar-width)*-1)]', // Adjust the padding for floating and inset variants. variant === 'floating' || variant === 'inset' ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]' : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l', className )} {...props} > <div data-sidebar="sidebar" data-slot="sidebar-inner" className="bg-sidebar group-data-[variant=floating]:ring-sidebar-border group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 flex size-full flex-col" > {children} </div> </div> </div> ); } function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<typeof Button>) { const { toggleSidebar } = useSidebar(); return ( <Button data-sidebar="trigger" data-slot="sidebar-trigger" variant="ghost" size="icon-sm" className={cn(className)} onClick={(event) => { onClick?.(event); toggleSidebar(); }} {...props} > <PanelLeftIcon /> <span className="sr-only">Toggle Sidebar</span> </Button> ); } function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) { const { toggleSidebar } = useSidebar(); return ( <button data-sidebar="rail" data-slot="sidebar-rail" aria-label="Toggle Sidebar" tabIndex={-1} onClick={toggleSidebar} title="Toggle Sidebar" className={cn( 'hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex', 'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize', '[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize', 'hover:group-data-[collapsible=offExamples]:bg-sidebar group-data-[collapsible=offExamples]:translate-x-0 group-data-[collapsible=offExamples]:after:left-full', '[[data-side=left][data-collapsible=offExamples]_&]:-right-2', '[[data-side=right][data-collapsible=offExamples]_&]:-left-2', className )} {...props} /> ); } function SidebarInset({ className, ...props }: React.ComponentProps<'main'>) { return ( <main data-slot="sidebar-inset" className={cn( 'bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2 relative flex w-full flex-1 flex-col', className )} {...props} /> ); } function SidebarInput({ className, ...props }: React.ComponentProps<typeof Input>) { return ( <Input data-slot="sidebar-input" data-sidebar="input" className={cn('bg-background h-8 w-full shadow-none', className)} {...props} /> ); } function SidebarHeader({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="sidebar-header" data-sidebar="header" className={cn('gap-2 p-2 flex flex-col', className)} {...props} /> ); } function SidebarFooter({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="sidebar-footer" data-sidebar="footer" className={cn('gap-2 p-2 flex flex-col', className)} {...props} /> ); } function SidebarSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) { return ( <Separator data-slot="sidebar-separator" data-sidebar="separator" className={cn('bg-sidebar-border mx-2 w-auto', className)} {...props} /> ); } function SidebarContent({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="sidebar-content" data-sidebar="content" className={cn( 'no-scrollbar gap-2 flex min-h-0 flex-1 flex-col overflow-auto group-data-[collapsible=icon]:overflow-hidden', className )} {...props} /> ); } function SidebarGroup({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="sidebar-group" data-sidebar="group" className={cn('p-2 relative flex w-full min-w-0 flex-col', className)} {...props} /> ); } function SidebarGroupLabel({ className, render, ...props }: useRender.ComponentProps<'div'> & React.ComponentProps<'div'>) { return useRender({ defaultTagName: 'div', props: mergeProps<'div'>( { className: cn( 'text-sidebar-foreground/70 ring-sidebar-ring h-8 rounded-md px-2 text-xs font-medium transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 flex shrink-0 items-center outline-hidden [&>svg]:shrink-0', className ) }, props ), render, state: { slot: 'sidebar-group-label', sidebar: 'group-label' } }); } function SidebarGroupAction({ className, render, ...props }: useRender.ComponentProps<'button'> & React.ComponentProps<'button'>) { return useRender({ defaultTagName: 'button', props: mergeProps<'button'>( { className: cn( 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 w-5 rounded-md p-0 focus-visible:ring-2 [&>svg]:size-4 flex aspect-square items-center justify-center outline-hidden transition-transform [&>svg]:shrink-0 after:absolute after:-inset-2 md:after:hidden group-data-[collapsible=icon]:hidden', className ) }, props ), render, state: { slot: 'sidebar-group-action', sidebar: 'group-action' } }); } function SidebarGroupContent({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="sidebar-group-content" data-sidebar="group-content" className={cn('text-sm w-full', className)} {...props} /> ); } function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) { return ( <ul data-slot="sidebar-menu" data-sidebar="menu" className={cn('gap-1 flex w-full min-w-0 flex-col', className)} {...props} /> ); } function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) { return ( <li data-slot="sidebar-menu-item" data-sidebar="menu-item" className={cn('group/menu-item relative', className)} {...props} /> ); } const sidebarMenuButtonVariants = cva( 'ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground gap-2 rounded-md p-2 text-left text-sm transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 data-active:font-medium peer/menu-button flex w-full items-center overflow-hidden outline-hidden group/menu-button disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&_svg]:size-4 [&_svg]:shrink-0', { variants: { variant: { default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground', outline: 'bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]' }, size: { default: 'h-8 text-sm', sm: 'h-7 text-xs', lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!' } }, defaultVariants: { variant: 'default', size: 'default' } } ); function SidebarMenuButton({ render, isActive = false, variant = 'default', size = 'default', tooltip, className, ...props }: useRender.ComponentProps<'button'> & React.ComponentProps<'button'> & { isActive?: boolean; tooltip?: string | React.ComponentProps<typeof TooltipContent>; } & VariantProps<typeof sidebarMenuButtonVariants>) { const { isMobile, state } = useSidebar(); const comp = useRender({ defaultTagName: 'button', props: mergeProps<'button'>( { className: cn(sidebarMenuButtonVariants({ variant, size }), className) }, props ), render: !tooltip ? render : TooltipTrigger, state: { slot: 'sidebar-menu-button', sidebar: 'menu-button', size, active: isActive } }); if (!tooltip) { return comp; } if (typeof tooltip === 'string') { tooltip = { children: tooltip }; } return ( <Tooltip> {comp} <TooltipContent side="right" align="center" hidden={state !== 'collapsed' || isMobile} {...tooltip} /> </Tooltip> ); } function SidebarMenuAction({ className, render, showOnHover = false, ...props }: useRender.ComponentProps<'button'> & React.ComponentProps<'button'> & { showOnHover?: boolean; }) { return useRender({ defaultTagName: 'button', props: mergeProps<'button'>( { className: cn( 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 aspect-square w-5 rounded-md p-0 peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 focus-visible:ring-2 [&>svg]:size-4 flex items-center justify-center outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 md:after:hidden [&>svg]:shrink-0', showOnHover && 'peer-data-active/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-open:opacity-100 md:opacity-0', className ) }, props ), render, state: { slot: 'sidebar-menu-action', sidebar: 'menu-action' } }); } function SidebarMenuBadge({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="sidebar-menu-badge" data-sidebar="menu-badge" className={cn( 'text-sidebar-foreground peer-hover/menu-button:text-sidebar-accent-foreground peer-data-active/menu-button:text-sidebar-accent-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 rounded-md px-1 text-xs font-medium peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 flex items-center justify-center tabular-nums select-none group-data-[collapsible=icon]:hidden', className )} {...props} /> ); } function SidebarMenuSkeleton({ className, showIcon = false, ...props }: React.ComponentProps<'div'> & { showIcon?: boolean; }) { // Random width between 50 to 90%. const [width] = React.useState(() => { return `${Math.floor(Math.random() * 40) + 50}%`; }); return ( <div data-slot="sidebar-menu-skeleton" data-sidebar="menu-skeleton" className={cn('h-8 gap-2 rounded-md px-2 flex items-center', className)} {...props} > {showIcon && ( <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" /> )} <Skeleton className="h-4 max-w-(--skeleton-width) flex-1" data-sidebar="menu-skeleton-text" style={ { '--skeleton-width': width } as React.CSSProperties } /> </div> ); } function SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) { return ( <ul data-slot="sidebar-menu-sub" data-sidebar="menu-sub" className={cn( 'border-sidebar-border mx-3.5 translate-x-px gap-1 border-l px-2.5 py-0.5 group-data-[collapsible=icon]:hidden flex min-w-0 flex-col', className )} {...props} /> ); } function SidebarMenuSubItem({ className, ...props }: React.ComponentProps<'li'>) { return ( <li data-slot="sidebar-menu-sub-item" data-sidebar="menu-sub-item" className={cn('group/menu-sub-item relative', className)} {...props} /> ); } function SidebarMenuSubButton({ render, size = 'md', isActive = false, className, ...props }: useRender.ComponentProps<'a'> & React.ComponentProps<'a'> & { size?: 'sm' | 'md'; isActive?: boolean; }) { return useRender({ defaultTagName: 'a', props: mergeProps<'a'>( { className: cn( 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground h-7 gap-2 rounded-md px-2 focus-visible:ring-2 data-[size=md]:text-sm data-[size=sm]:text-xs [&>svg]:size-4 flex min-w-0 -translate-x-px items-center overflow-hidden outline-hidden group-data-[collapsible=icon]:hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:shrink-0', className ) }, props ), render, state: { slot: 'sidebar-menu-sub-button', sidebar: 'menu-sub-button', size, active: isActive } }); } export { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarSeparator, SidebarTrigger, useSidebar }; ================================================ FILE: packages/evershop/src/components/common/ui/Skeleton.tsx ================================================ import { cn } from '@evershop/evershop/lib/util/cn'; import React from 'react'; function Skeleton({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="skeleton" className={cn('bg-muted rounded-md animate-pulse', className)} {...props} /> ); } export { Skeleton }; ================================================ FILE: packages/evershop/src/components/common/ui/Slider.tsx ================================================ import { Slider as SliderPrimitive } from '@base-ui/react/slider'; import { cn } from '@evershop/evershop/lib/util/cn'; import * as React from 'react'; function Slider({ className, defaultValue, value, min = 0, max = 100, ...props }: SliderPrimitive.Root.Props) { const _values = React.useMemo( () => Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max], [value, defaultValue, min, max] ); return ( <SliderPrimitive.Root className="data-horizontal:w-full data-vertical:h-full" data-slot="slider" defaultValue={defaultValue} value={value} min={min} max={max} thumbAlignment="edge" {...props} > <SliderPrimitive.Control className={cn( 'data-vertical:min-h-40 relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-vertical:h-full data-vertical:w-auto data-vertical:flex-col', className )} > <SliderPrimitive.Track data-slot="slider-track" className="bg-muted rounded-full data-horizontal:h-1.5 data-horizontal:w-full data-vertical:h-full data-vertical:w-1.5 relative overflow-hidden select-none" > <SliderPrimitive.Indicator data-slot="slider-range" className="bg-primary select-none data-horizontal:h-full data-vertical:w-full" /> </SliderPrimitive.Track> {Array.from({ length: _values.length }, (_, index) => ( <SliderPrimitive.Thumb data-slot="slider-thumb" key={index} className="border-primary ring-ring/50 size-4 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden block shrink-0 select-none disabled:pointer-events-none disabled:opacity-50" /> ))} </SliderPrimitive.Control> </SliderPrimitive.Root> ); } export { Slider }; ================================================ FILE: packages/evershop/src/components/common/ui/Spinner.tsx ================================================ import { cn } from '@evershop/evershop/lib/util/cn'; import { Loader2Icon } from 'lucide-react'; import React from 'react'; function Spinner({ className, ...props }: React.ComponentProps<'svg'>) { return ( <Loader2Icon role="status" aria-label="Loading" className={cn('size-4 animate-spin', className)} {...props} /> ); } export { Spinner }; ================================================ FILE: packages/evershop/src/components/common/ui/Switch.tsx ================================================ import { Switch as SwitchPrimitive } from '@base-ui/react/switch'; import { cn } from '@evershop/evershop/lib/util/cn'; import React from 'react'; function Switch({ className, size = 'default', ...props }: SwitchPrimitive.Root.Props & { size?: 'sm' | 'default'; }) { return ( <SwitchPrimitive.Root data-slot="switch" data-size={size} className={cn( 'data-checked:bg-primary data-unchecked:bg-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 dark:data-unchecked:bg-input/80 shrink-0 rounded-full border border-transparent shadow-xs focus-visible:ring-[3px] aria-invalid:ring-[3px] data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] peer group/switch relative inline-flex items-center transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 data-disabled:cursor-not-allowed data-disabled:opacity-50', className )} {...props} > <SwitchPrimitive.Thumb data-slot="switch-thumb" className="bg-background dark:data-unchecked:bg-foreground dark:data-checked:bg-primary-foreground rounded-full group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 pointer-events-none block ring-0 transition-transform" /> </SwitchPrimitive.Root> ); } export { Switch }; ================================================ FILE: packages/evershop/src/components/common/ui/Table.tsx ================================================ import { cn } from '@evershop/evershop/lib/util/cn'; import * as React from 'react'; function Table({ className, ...props }: React.ComponentProps<'table'>) { return ( <div data-slot="table-container" className="relative w-full"> <table data-slot="table" className={cn('w-full caption-bottom text-sm', className)} {...props} /> </div> ); } function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) { return ( <thead data-slot="table-header" className={cn('[&_tr]:border-b', className)} {...props} /> ); } function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) { return ( <tbody data-slot="table-body" className={cn('[&_tr:last-child]:border-0', className)} {...props} /> ); } function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) { return ( <tfoot data-slot="table-footer" className={cn( 'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0', className )} {...props} /> ); } function TableRow({ className, ...props }: React.ComponentProps<'tr'>) { return ( <tr data-slot="table-row" className={cn( 'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors border-border', className )} {...props} /> ); } function TableHead({ className, ...props }: React.ComponentProps<'th'>) { return ( <th data-slot="table-head" className={cn( 'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0', className )} {...props} /> ); } function TableCell({ className, ...props }: React.ComponentProps<'td'>) { return ( <td data-slot="table-cell" className={cn( 'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0', className )} {...props} /> ); } function TableCaption({ className, ...props }: React.ComponentProps<'caption'>) { return ( <caption data-slot="table-caption" className={cn('text-muted-foreground mt-4 text-sm', className)} {...props} /> ); } export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }; ================================================ FILE: packages/evershop/src/components/common/ui/Tabs.tsx ================================================ import { Tabs as TabsPrimitive } from '@base-ui/react/tabs'; import { cn } from '@evershop/evershop/lib/util/cn'; import { cva, type VariantProps } from 'class-variance-authority'; import React from 'react'; function Tabs({ className, orientation = 'horizontal', ...props }: TabsPrimitive.Root.Props) { return ( <TabsPrimitive.Root data-slot="tabs" data-orientation={orientation} className={cn( 'gap-2 group/tabs flex data-[orientation=horizontal]:flex-col', className )} {...props} /> ); } const tabsListVariants = cva( 'rounded-lg p-[3px] group-data-horizontal/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col', { variants: { variant: { default: 'bg-muted', line: 'gap-1 bg-transparent' } }, defaultVariants: { variant: 'default' } } ); function TabsList({ className, variant = 'default', ...props }: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>) { return ( <TabsPrimitive.List data-slot="tabs-list" data-variant={variant} className={cn(tabsListVariants({ variant }), className)} {...props} /> ); } function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) { return ( <TabsPrimitive.Tab data-slot="tabs-trigger" className={cn( "gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg:not([class*='size-'])]:size-4 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", 'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent', 'data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground', 'after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100', className )} {...props} /> ); } function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) { return ( <TabsPrimitive.Panel data-slot="tabs-content" className={cn('text-sm flex-1 outline-none', className)} {...props} /> ); } export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }; ================================================ FILE: packages/evershop/src/components/common/ui/Textarea.tsx ================================================ import { cn } from '@evershop/evershop/lib/util/cn'; import * as React from 'react'; const Textarea = React.forwardRef< HTMLTextAreaElement, React.ComponentProps<'textarea'> >(({ className, ...props }, ref) => { return ( <textarea ref={ref} data-slot="textarea" className={cn( 'border-input dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border bg-transparent px-2.5 py-2 text-base shadow-xs transition-[color,box-shadow] focus-visible:ring-[3px] aria-invalid:ring-[3px] md:text-sm placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full outline-none disabled:cursor-not-allowed disabled:opacity-50', className )} {...props} /> ); }); Textarea.displayName = 'Textarea'; export { Textarea }; ================================================ FILE: packages/evershop/src/components/common/ui/Toggle.tsx ================================================ import { Toggle as TogglePrimitive } from '@base-ui/react/toggle'; import { cn } from '@evershop/evershop/lib/util/cn'; import { cva, type VariantProps } from 'class-variance-authority'; import React from 'react'; const toggleVariants = cva( "hover:text-foreground aria-pressed:bg-muted focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive gap-1 rounded-md text-sm font-medium transition-[color,box-shadow] [&_svg:not([class*='size-'])]:size-4 group/toggle hover:bg-muted inline-flex items-center justify-center whitespace-nowrap outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", { variants: { variant: { default: 'bg-transparent', outline: 'border-input hover:bg-muted border bg-transparent shadow-xs' }, size: { default: 'h-9 min-w-9 px-2', sm: 'h-8 min-w-8 px-1.5', lg: 'h-10 min-w-10 px-2.5' } }, defaultVariants: { variant: 'default', size: 'default' } } ); function Toggle({ className, variant = 'default', size = 'default', ...props }: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) { return ( <TogglePrimitive data-slot="toggle" className={cn(toggleVariants({ variant, size, className }))} {...props} /> ); } export { Toggle, toggleVariants }; ================================================ FILE: packages/evershop/src/components/common/ui/ToggleGroup.tsx ================================================ import { Toggle as TogglePrimitive } from '@base-ui/react/toggle'; import { ToggleGroup as ToggleGroupPrimitive } from '@base-ui/react/toggle-group'; import { toggleVariants } from '@components/common/ui/Toggle.js'; import { cn } from '@evershop/evershop/lib/util/cn'; import { type VariantProps } from 'class-variance-authority'; import * as React from 'react'; const ToggleGroupContext = React.createContext< VariantProps<typeof toggleVariants> & { spacing?: number; orientation?: 'horizontal' | 'vertical'; } >({ size: 'default', variant: 'default', spacing: 0, orientation: 'horizontal' }); function ToggleGroup({ className, variant, size, spacing = 0, orientation = 'horizontal', children, ...props }: ToggleGroupPrimitive.Props & VariantProps<typeof toggleVariants> & { spacing?: number; orientation?: 'horizontal' | 'vertical'; }) { return ( <ToggleGroupPrimitive data-slot="toggle-group" data-variant={variant} data-size={size} data-spacing={spacing} data-orientation={orientation} style={{ '--gap': spacing } as React.CSSProperties} className={cn( 'rounded-md data-[spacing=0]:data-[variant=outline]:shadow-xs group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch', className )} {...props} > <ToggleGroupContext.Provider value={{ variant, size, spacing, orientation }} > {children} </ToggleGroupContext.Provider> </ToggleGroupPrimitive> ); } function ToggleGroupItem({ className, children, variant = 'default', size = 'default', ...props }: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) { const context = React.useContext(ToggleGroupContext); return ( <TogglePrimitive data-slot="toggle-group-item" data-variant={context.variant || variant} data-size={context.size || size} data-spacing={context.spacing} className={cn( 'data-[state=on]:bg-muted group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 group-data-[spacing=0]/toggle-group:shadow-none group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-md group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-md group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-md group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-md shrink-0 focus:z-10 focus-visible:z-10 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t', toggleVariants({ variant: context.variant || variant, size: context.size || size }), className )} {...props} > {children} </TogglePrimitive> ); } export { ToggleGroup, ToggleGroupItem }; ================================================ FILE: packages/evershop/src/components/common/ui/Tooltip.tsx ================================================ import { Tooltip as TooltipPrimitive } from '@base-ui/react/tooltip'; import { cn } from '@evershop/evershop/lib/util/cn'; import React from 'react'; function TooltipProvider({ delay = 0, ...props }: TooltipPrimitive.Provider.Props) { return ( <TooltipPrimitive.Provider data-slot="tooltip-provider" delay={delay} {...props} /> ); } function Tooltip({ ...props }: TooltipPrimitive.Root.Props) { return ( <TooltipProvider> <TooltipPrimitive.Root data-slot="tooltip" {...props} /> </TooltipProvider> ); } function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) { return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />; } function TooltipContent({ className, side = 'top', sideOffset = 4, align = 'center', alignOffset = 0, children, ...props }: TooltipPrimitive.Popup.Props & Pick< TooltipPrimitive.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset' >) { return ( <TooltipPrimitive.Portal> <TooltipPrimitive.Positioner align={align} alignOffset={alignOffset} side={side} sideOffset={sideOffset} className="isolate z-50" > <TooltipPrimitive.Popup data-slot="tooltip-content" className={cn( 'data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 rounded-md px-3 py-1.5 text-xs bg-foreground text-background z-50 w-fit max-w-xs origin-(--transform-origin)', className )} {...props} > {children} <TooltipPrimitive.Arrow className="size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground z-50 data-[side=bottom]:top-1 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" /> </TooltipPrimitive.Popup> </TooltipPrimitive.Positioner> </TooltipPrimitive.Portal> ); } export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; ================================================ FILE: packages/evershop/src/components/common/ui/hooks/useIsMobile.tsx ================================================ import * as React from 'react'; const MOBILE_BREAKPOINT = 768; export function useIsMobile() { const [isMobile, setIsMobile] = React.useState<boolean | undefined>( undefined ); React.useEffect(() => { const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); const onChange = () => { setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); }; mql.addEventListener('change', onChange); setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); return () => mql.removeEventListener('change', onChange); }, []); return !!isMobile; } ================================================ FILE: packages/evershop/src/components/frontStore/Coupon.tsx ================================================ import { useCartDispatch, useCartState } from '@components/frontStore/cart/CartContext.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React, { useState, useCallback } from 'react'; export interface CouponState { isLoading: boolean; error: string | null; appliedCoupon: string | null; canApplyCoupon: boolean; canRemoveCoupon: boolean; hasActiveCoupon: boolean; } export interface CouponActions { applyCoupon: (code: string) => Promise<void>; removeCoupon: () => Promise<void>; clearError: () => void; } export interface CouponProps { onApplySuccess?: (couponCode: string) => void; onRemoveSuccess?: () => void; onError?: (error: string) => void; children: (state: CouponState, actions: CouponActions) => React.ReactNode; } export const Coupon: React.FC<CouponProps> = ({ onApplySuccess, onRemoveSuccess, onError, children }) => { const cartDispatch = useCartDispatch(); const cartState = useCartState(); const [localError, setLocalError] = useState<string | null>(null); const appliedCoupon = cartState.data?.coupon || null; const hasActiveCoupon = !!appliedCoupon && appliedCoupon.trim() !== ''; const canApplyCoupon = !!cartState.data && !hasActiveCoupon; const canRemoveCoupon = !!cartState.data && hasActiveCoupon; const isLoading = cartState.loading; const clearError = useCallback(() => { setLocalError(null); cartDispatch.clearError(); }, [cartDispatch]); const applyCoupon = useCallback( async (code: string) => { if (!canApplyCoupon || !code.trim()) { const errorMsg = !cartState.data ? _('Cart is not initialized') : hasActiveCoupon ? _('A coupon is already applied') : _('Please enter a coupon code'); setLocalError(errorMsg); onError?.(errorMsg); return; } try { setLocalError(null); cartDispatch.clearError(); await cartDispatch.applyCoupon(code.trim()); onApplySuccess?.(code.trim()); } catch (error) { const errorMessage = error instanceof Error ? error.message : _('Failed to apply coupon'); setLocalError(errorMessage); onError?.(errorMessage); } }, [ canApplyCoupon, cartState.data, hasActiveCoupon, cartDispatch, onApplySuccess, onError ] ); const removeCoupon = useCallback(async () => { if (!canRemoveCoupon) { const errorMsg = !cartState.data ? _('Cart is not initialized') : _('No coupon to remove'); setLocalError(errorMsg); onError?.(errorMsg); return; } try { setLocalError(null); cartDispatch.clearError(); await cartDispatch.removeCoupon(); onRemoveSuccess?.(); } catch (error) { const errorMessage = error instanceof Error ? error.message : _('Failed to remove coupon'); setLocalError(errorMessage); onError?.(errorMessage); } }, [canRemoveCoupon, cartState.data, cartDispatch, onRemoveSuccess, onError]); const state: CouponState = { isLoading, error: localError || cartState.data?.error || null, appliedCoupon, canApplyCoupon, canRemoveCoupon, hasActiveCoupon }; const actions: CouponActions = { applyCoupon, removeCoupon, clearError }; return <>{children(state, actions)}</>; }; ================================================ FILE: packages/evershop/src/components/frontStore/CouponForm.tsx ================================================ import { Form } from '@components/common/form/Form.js'; import { InputField } from '@components/common/form/InputField.js'; import { Button } from '@components/common/ui/Button.js'; import { Coupon, CouponActions, CouponState } from '@components/frontStore/Coupon.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; import { useForm } from 'react-hook-form'; import { toast } from 'react-toastify'; export function CouponForm() { const form = useForm<{ coupon: string }>(); const coupon = form.watch('coupon'); return ( <Coupon onApplySuccess={() => { toast.success(_('Coupon applied successfully!')); }} onError={() => { toast.error(_('Invalid coupon')); }} onRemoveSuccess={() => { toast.success(_('Coupon removed successfully!')); }} > {(state: CouponState, actions: CouponActions) => ( <div className="coupon-form"> <Form form={form} method="POST" submitBtn={false}> <div className="flex justify-between gap-3"> <div className="w-4/5"> <InputField name="coupon" required validation={{ required: { value: true, message: _('Coupon code is required') } }} defaultValue={state.appliedCoupon || ''} disabled={!!state.appliedCoupon} placeholder={_('Enter coupon code')} wrapperClassName="mb-0 form-field" /> </div> <div className="col-span-1"> <Button isLoading={state.isLoading} onClick={async () => { if (state.appliedCoupon) { await actions.removeCoupon(); } else { const isValid = await form.trigger(); if (isValid) { actions.applyCoupon(coupon); } } }} variant={state.appliedCoupon ? 'destructive' : 'default'} > {state.appliedCoupon ? _('Remove') : _('Apply')} </Button> </div> </div> </Form> </div> )} </Coupon> ); } ================================================ FILE: packages/evershop/src/components/frontStore/Footer.tsx ================================================ import Area from '@components/common/Area.js'; import React from 'react'; interface FooterProps { copyRight: string; } export function Footer({ copyRight }: FooterProps) { return ( <footer className="footer bg-gray-100 mt-24 pt-2.5 pb-2.5 border-t border-gray-300"> <Area id="footerTop" className="footer__top" /> <div className="footer__middle flex justify-between items-center"> <Area id="footerMiddleLeft" className="footer__middle__left" /> <Area id="footerMiddleCenter" className="footer__middle__center" /> <Area id="footerMiddleRight" className="footer__middle__right" /> </div> <Area id="footerBottom" className="footer__bottom" coreComponents={[ { component: { default: ( <div className="page-width grid grid-cols-1 md:grid-cols-2 gap-5 justify-between"> <div> <div className="card-icons flex justify-center space-x-2 md:justify-start"> <div> <svg xmlns="http://www.w3.org/2000/svg" width="38" height="24" aria-labelledby="pi-visa" viewBox="0 0 38 24" className="h-10" > <path d="M35 0H3C1.3 0 0 1.3 0 3v18c0 1.7 1.4 3 3 3h32c1.7 0 3-1.3 3-3V3c0-1.7-1.4-3-3-3z" opacity="0.07" /> <path fill="#fff" d="M35 1c1.1 0 2 .9 2 2v18c0 1.1-.9 2-2 2H3c-1.1 0-2-.9-2-2V3c0-1.1.9-2 2-2h32" /> <path fill="#142688" d="M28.3 10.1H28c-.4 1-.7 1.5-1 3h1.9c-.3-1.5-.3-2.2-.6-3zm2.9 5.9h-1.7c-.1 0-.1 0-.2-.1l-.2-.9-.1-.2h-2.4c-.1 0-.2 0-.2.2l-.3.9c0 .1-.1.1-.1.1h-2.1l.2-.5L27 8.7c0-.5.3-.7.8-.7h1.5c.1 0 .2 0 .2.2l1.4 6.5c.1.4.2.7.2 1.1.1.1.1.1.1.2zm-13.4-.3l.4-1.8c.1 0 .2.1.2.1.7.3 1.4.5 2.1.4.2 0 .5-.1.7-.2.5-.2.5-.7.1-1.1-.2-.2-.5-.3-.8-.5-.4-.2-.8-.4-1.1-.7-1.2-1-.8-2.4-.1-3.1.6-.4.9-.8 1.7-.8 1.2 0 2.5 0 3.1.2h.1c-.1.6-.2 1.1-.4 1.7-.5-.2-1-.4-1.5-.4-.3 0-.6 0-.9.1-.2 0-.3.1-.4.2-.2.2-.2.5 0 .7l.5.4c.4.2.8.4 1.1.6.5.3 1 .8 1.1 1.4.2.9-.1 1.7-.9 2.3-.5.4-.7.6-1.4.6-1.4 0-2.5.1-3.4-.2-.1.2-.1.2-.2.1zm-3.5.3c.1-.7.1-.7.2-1 .5-2.2 1-4.5 1.4-6.7.1-.2.1-.3.3-.3H18c-.2 1.2-.4 2.1-.7 3.2-.3 1.5-.6 3-1 4.5 0 .2-.1.2-.3.2M5 8.2c0-.1.2-.2.3-.2h3.4c.5 0 .9.3 1 .8l.9 4.4c0 .1 0 .1.1.2 0-.1.1-.1.1-.1l2.1-5.1c-.1-.1 0-.2.1-.2h2.1c0 .1 0 .1-.1.2l-3.1 7.3c-.1.2-.1.3-.2.4-.1.1-.3 0-.5 0H9.7c-.1 0-.2 0-.2-.2L7.9 9.5c-.2-.2-.5-.5-.9-.6-.6-.3-1.7-.5-1.9-.5L5 8.2z" /> </svg> </div> <div> <svg xmlns="http://www.w3.org/2000/svg" width="38" height="24" aria-labelledby="pi-master" viewBox="0 0 38 24" className="h-10" > <path d="M35 0H3C1.3 0 0 1.3 0 3v18c0 1.7 1.4 3 3 3h32c1.7 0 3-1.3 3-3V3c0-1.7-1.4-3-3-3z" opacity="0.07" /> <path fill="#fff" d="M35 1c1.1 0 2 .9 2 2v18c0 1.1-.9 2-2 2H3c-1.1 0-2-.9-2-2V3c0-1.1.9-2 2-2h32" /> <circle cx="15" cy="12" r="7" fill="#EB001B" /> <circle cx="23" cy="12" r="7" fill="#F79E1B" /> <path fill="#FF5F00" d="M22 12c0-2.4-1.2-4.5-3-5.7-1.8 1.3-3 3.4-3 5.7s1.2 4.5 3 5.7c1.8-1.2 3-3.3 3-5.7z" /> </svg> </div> <div> <svg viewBox="0 0 38 24" xmlns="http://www.w3.org/2000/svg" width="38" height="24" role="img" aria-labelledby="pi-paypal" className="h-10" > <title id="pi-paypal">PayPal
    {copyRight}
    ) }, sortOrder: 10 } ]} /> ); } ================================================ FILE: packages/evershop/src/components/frontStore/Header.scss ================================================ .header { padding: 1rem; border-bottom: 1px solid #ebebeb; @media only screen and (min-width: 1200px) { padding-left: 50px; padding-right: 50px; } } ================================================ FILE: packages/evershop/src/components/frontStore/Header.tsx ================================================ import Area from '@components/common/Area.js'; import React from 'react'; export function Header() { return (
    ); } ================================================ FILE: packages/evershop/src/components/frontStore/Og.tsx ================================================ import { Meta } from '@components/common/Meta.js'; import React from 'react'; export interface OgProps { type?: 'website' | 'article' | 'product' | string; /** * The title of the page to be displayed when shared */ title?: string; /** * A brief description of the page content */ description?: string; /** * URL to an image that represents the page * Recommended size: 1200x630 pixels for best display across platforms */ image?: string; /** * The canonical URL of the page */ url?: string; /** * The name of the website or app */ siteName?: string; /** * For article type, the published date in ISO format */ publishedTime?: string; /** * For article type, author names or URLs */ authors?: string[]; /** * Locale code for the content (e.g., 'en_US') */ locale?: string; /** * Alternative locales available for the page */ alternateLocales?: string[]; twitterCard?: 'summary' | 'summary_large_image' | 'app' | 'player'; twitterSite?: string; /** * Twitter @username of the content creator */ twitterCreator?: string; twitterImage?: string; /** * Whether to include Twitter card tags */ includeTwitterTags?: boolean; } export function Og({ type = 'website', title, description, image, url, siteName, publishedTime, authors, locale, alternateLocales, twitterCard = 'summary', twitterSite, twitterCreator, twitterImage, includeTwitterTags = true }: OgProps) { return ( <> {title && } {description && } {image && } {url && } {siteName && } {type === 'article' && publishedTime && ( )} {type === 'article' && authors?.length && authors.map((author, index) => ( ))} {locale && } {alternateLocales?.length && alternateLocales.map((alternateLocale, index) => ( ))} {includeTwitterTags && ( <> {title && } {description && ( )} {twitterSite && } {twitterCreator && ( )} {twitterImage && } )} ); } ================================================ FILE: packages/evershop/src/components/frontStore/Pagination.tsx ================================================ import { useAppDispatch } from '@components/common/context/app.js'; import { Pagination as PaginationUI, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from '@components/common/ui/Pagination.js'; import React, { useState, useEffect, useCallback } from 'react'; export interface PaginationProps { total: number; limit: number; currentPage: number; children: (props: PaginationRenderProps) => React.ReactNode; onPageChange?: (page: number) => void; scrollToTop?: boolean; scrollBehavior?: 'auto' | 'smooth'; } export interface PaginationRenderProps { currentPage: number; totalPages: number; total: number; limit: number; hasNext: boolean; hasPrev: boolean; startItem: number; endItem: number; goToPage: (page: number) => Promise; goToNext: () => Promise; goToPrev: () => Promise; goToFirst: () => Promise; goToLast: () => Promise; getPageNumbers: (range?: number) => number[]; isCurrentPage: (page: number) => boolean; isValidPage: (page: number) => boolean; isLoading: boolean; getDisplayText: () => string; getPageInfo: () => { showing: string; total: string; }; } export const PaginationContext = React.createContext<{ goToPage: (page: number) => Promise; } | null>(null); export const usePagination = () => { const context = React.useContext(PaginationContext); if (!context) { throw new Error('usePagination must be used within a PaginationProvider'); } return context; }; export const usePaginationLogic = ( total: number, limit: number, initialPage: number, onPageChange?: (page: number) => void, scrollToTop: boolean = true, scrollBehavior: 'auto' | 'smooth' = 'smooth' ) => { const AppContextDispatch = useAppDispatch(); const [page, setPage] = useState(initialPage); const [isLoading, setIsLoading] = useState(false); const totalPages = Math.ceil(total / limit); useEffect(() => { setPage(initialPage); }, [initialPage]); const navigateToPage = useCallback( async (pageNum: number) => { if (isLoading) return; let validPage; if (pageNum < 1) validPage = 1; else if (pageNum > totalPages) validPage = totalPages; else validPage = pageNum; if (validPage === page) return; setIsLoading(true); try { const url = new URL(window.location.href, window.location.origin); url.searchParams.set('page', validPage.toString()); url.searchParams.append('ajax', 'true'); setPage(validPage); await AppContextDispatch.fetchPageData(url); url.searchParams.delete('ajax'); history.pushState(null, '', url); if (scrollToTop) { window.scrollTo({ top: 0, behavior: scrollBehavior }); } onPageChange?.(validPage); } catch (error) { setPage(page); } finally { setIsLoading(false); } }, [ AppContextDispatch, page, totalPages, isLoading, onPageChange, scrollToTop, scrollBehavior ] ); const goToPage = useCallback( (pageNum: number) => navigateToPage(pageNum), [navigateToPage] ); const goToNext = useCallback( () => navigateToPage(page + 1), [navigateToPage, page] ); const goToPrev = useCallback( () => navigateToPage(page - 1), [navigateToPage, page] ); const goToFirst = useCallback(() => navigateToPage(1), [navigateToPage]); const goToLast = useCallback( () => navigateToPage(totalPages), [navigateToPage, totalPages] ); const getPageNumbers = useCallback( (range: number = 5) => { const pages: number[] = []; const half = Math.floor(range / 2); let start = Math.max(1, page - half); const end = Math.min(totalPages, start + range - 1); // Adjust start if we're near the end if (end - start + 1 < range) { start = Math.max(1, end - range + 1); } for (let i = start; i <= end; i++) { pages.push(i); } return pages; }, [page, totalPages] ); const isCurrentPage = useCallback( (pageNum: number) => pageNum === page, [page] ); const isValidPage = useCallback( (pageNum: number) => pageNum >= 1 && pageNum <= totalPages, [totalPages] ); const getDisplayText = useCallback(() => { const start = (page - 1) * limit + 1; const end = Math.min(page * limit, total); return `Showing ${start}-${end} of ${total} results`; }, [page, limit, total]); const getPageInfo = useCallback(() => { const start = (page - 1) * limit + 1; const end = Math.min(page * limit, total); return { showing: `${start}-${end}`, total: total.toString() }; }, [page, limit, total]); return { currentPage: page, totalPages, hasNext: page < totalPages, hasPrev: page > 1, startItem: (page - 1) * limit + 1, endItem: Math.min(page * limit, total), goToPage, goToNext, goToPrev, goToFirst, goToLast, getPageNumbers, isCurrentPage, isValidPage, isLoading, getDisplayText, getPageInfo }; }; // Main Pagination component with render prop pattern export function Pagination({ total, limit, currentPage, children, onPageChange, scrollToTop = true, scrollBehavior = 'smooth' }: PaginationProps) { const paginationLogic = usePaginationLogic( total, limit, currentPage, onPageChange, scrollToTop, scrollBehavior ); // Prepare render props const renderProps: PaginationRenderProps = { // Pagination data currentPage: paginationLogic.currentPage, totalPages: paginationLogic.totalPages, total, limit, hasNext: paginationLogic.hasNext, hasPrev: paginationLogic.hasPrev, startItem: paginationLogic.startItem, endItem: paginationLogic.endItem, // Navigation functions goToPage: paginationLogic.goToPage, goToNext: paginationLogic.goToNext, goToPrev: paginationLogic.goToPrev, goToFirst: paginationLogic.goToFirst, goToLast: paginationLogic.goToLast, // Utility functions getPageNumbers: paginationLogic.getPageNumbers, isCurrentPage: paginationLogic.isCurrentPage, isValidPage: paginationLogic.isValidPage, // State isLoading: paginationLogic.isLoading, // Display helpers getDisplayText: paginationLogic.getDisplayText, getPageInfo: paginationLogic.getPageInfo }; const contextValue = React.useMemo( () => ({ goToPage: paginationLogic.goToPage }), [paginationLogic.goToPage] ); return ( {children(renderProps)} ); } // Default pagination renderer component (maintains original design) export const DefaultPaginationRenderer: React.FC<{ renderProps: PaginationRenderProps; className?: string; showInfo?: boolean; }> = ({ renderProps, className = '', showInfo = false }) => { const { currentPage, totalPages, hasNext, hasPrev, goToNext, goToPrev, goToPage, getPageNumbers, isCurrentPage, isLoading, getDisplayText } = renderProps; const pageNumbers = getPageNumbers(7); const showStartEllipsis = pageNumbers[0] > 1; const showEndEllipsis = pageNumbers[pageNumbers.length - 1] < totalPages; return (
    {showInfo && (
    {getDisplayText()}
    )} {hasPrev && ( { e.preventDefault(); if (!isLoading) goToPrev(); }} aria-disabled={isLoading} className={isLoading ? 'pointer-events-none opacity-50' : ''} /> )} {showStartEllipsis && ( <> { e.preventDefault(); if (!isLoading) goToPage(1); }} aria-disabled={isLoading} className={isLoading ? 'pointer-events-none opacity-50' : ''} > 1 )} {pageNumbers.map((p) => ( { e.preventDefault(); if (!isLoading && !isCurrentPage(p)) goToPage(p); }} isActive={isCurrentPage(p)} aria-disabled={isLoading || isCurrentPage(p)} className={ isLoading || isCurrentPage(p) ? 'pointer-events-none opacity-50' : '' } > {p} ))} {showEndEllipsis && ( <> { e.preventDefault(); if (!isLoading) goToPage(totalPages); }} aria-disabled={isLoading} className={isLoading ? 'pointer-events-none opacity-50' : ''} > {totalPages} )} {hasNext && ( { e.preventDefault(); if (!isLoading) goToNext(); }} aria-disabled={isLoading} className={isLoading ? 'pointer-events-none opacity-50' : ''} /> )} {isLoading && (
    )}
    ); }; // Compact pagination renderer export const CompactPaginationRenderer: React.FC<{ renderProps: PaginationRenderProps; className?: string; }> = ({ renderProps, className = '' }) => { const { currentPage, totalPages, hasNext, hasPrev, goToNext, goToPrev, getPageInfo, isLoading } = renderProps; const { showing, total } = getPageInfo(); return (
    Showing {showing} of {total}
    Page {currentPage} of {totalPages}
    ); }; export const InputPaginationRenderer: React.FC<{ renderProps: PaginationRenderProps; className?: string; }> = ({ renderProps, className = '' }) => { const { currentPage, totalPages, hasNext, hasPrev, goToNext, goToPrev, goToPage, goToFirst, goToLast, getDisplayText, isLoading } = renderProps; const [inputPage, setInputPage] = React.useState(currentPage.toString()); React.useEffect(() => { setInputPage(currentPage.toString()); }, [currentPage]); const handleInputSubmit = (e: React.FormEvent) => { e.preventDefault(); const page = parseInt(inputPage); if (!isNaN(page)) { goToPage(page); } }; return (
    {getDisplayText()}
    Page setInputPage(e.target.value)} className="w-16 px-2 py-1 text-sm border border-gray-300 rounded text-center" disabled={isLoading} /> of {totalPages}
    ); }; ================================================ FILE: packages/evershop/src/components/frontStore/cart/AddToCart.tsx ================================================ import { useCartDispatch, useCartState } from '@components/frontStore/cart/CartContext.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React, { useState, useCallback } from 'react'; export interface ProductInfo { sku: string; isInStock: boolean; } export interface AddToCartState { isLoading: boolean; error: string | null; canAddToCart: boolean; isInStock: boolean; } export interface AddToCartActions { addToCart: () => Promise; clearError: () => void; } export interface AddToCartProps { product: ProductInfo; qty: number; // Quantity to add to cart onSuccess?: (quantity: number) => void; onError?: (error: string) => void; children: ( state: AddToCartState, actions: AddToCartActions ) => React.ReactNode; } export const AddToCart: React.FC = ({ product, qty, onSuccess, onError, children }) => { const cartDispatch = useCartDispatch(); const cartState = useCartState(); const [localError, setLocalError] = useState(null); const canAddToCart = product.isInStock && qty > 0 && !!cartState.data; const isLoading = cartState.loading; const clearError = useCallback(() => { setLocalError(null); cartDispatch.clearError(); }, [cartDispatch]); const addToCart = useCallback(async () => { if (!canAddToCart) { const errorMsg = !product.isInStock ? _('Product is out of stock') : !cartState.data ? _('Cart is not initialized') : _('Invalid quantity'); setLocalError(errorMsg); onError?.(errorMsg); return; } try { setLocalError(null); cartDispatch.clearError(); await cartDispatch.addItem({ sku: product.sku, qty: qty }); onSuccess?.(qty); } catch (error) { const errorMessage = error instanceof Error ? error.message : _('Failed to add item to cart'); setLocalError(errorMessage); onError?.(errorMessage); } }, [ canAddToCart, product.isInStock, product.sku, qty, cartState.data, cartDispatch, onSuccess, onError ]); const state: AddToCartState = { isLoading, error: localError || cartState.data?.error || null, canAddToCart, isInStock: product.isInStock }; const actions: AddToCartActions = { addToCart, clearError }; return <>{children(state, actions)}; }; ================================================ FILE: packages/evershop/src/components/frontStore/cart/CartContext.tsx ================================================ import { _ } from '@evershop/evershop/lib/locale/translate/_'; import { ApiResponse } from '@evershop/evershop/types/apiResponse'; import { CustomerAddressGraphql, Address } from '@evershop/evershop/types/customerAddress'; import { produce } from 'immer'; import React, { createContext, useReducer, useContext, ReactNode, useCallback } from 'react'; import { useQuery, useClient } from 'urql'; const ShippingMethodsQuery = ` query GetCartShippingMethods($country: String!, $province: String, $postcode: String) { myCart { availableShippingMethods(country: $country, province: $province, postcode: $postcode) { code name cost { value text } } } } `; export enum CartSyncTrigger { ADD_ITEM = 'addItem', REMOVE_ITEM = 'removeItem', UPDATE_ITEM = 'updateItem', ADD_PAYMENT_METHOD = 'addPaymentMethod', ADD_SHIPPING_METHOD = 'addShippingMethod', ADD_SHIPPING_ADDRESS = 'addShippingAddress', ADD_BILLING_ADDRESS = 'addBillingAddress', ADD_CONTACT_INFO = 'addContactInfo', APPLY_COUPON = 'applyCoupon', REMOVE_COUPON = 'removeCoupon' } export interface CartItem { cartItemId: string; uuid: string; productId: string; qty: number; productSku: string; productName: string; productUrl: string; thumbnail?: string; noShippingRequired: boolean; productWeight: { value: number; unit: string; }; variantOptions?: { attributeCode: string; attributeName: string; attributeId: number; optionId: number; optionText: string; }[]; productPrice: { value: number; text: string; }; productPriceInclTax: { value: number; text: string; }; finalPrice: { value: number; text: string; }; finalPriceInclTax: { value: number; text: string; }; taxPercent: number; taxAmount: { value: number; text: string; }; taxAmountBeforeDiscount: { value: number; text: string; }; discountAmount: { value: number; text: string; }; lineTotal: { value: number; text: string; }; subTotal: { value: number; text: string; }; lineTotalWithDiscount: { value: number; text: string; }; lineTotalWithDiscountInclTax: { value: number; text: string; }; lineTotalInclTax: { value: number; text: string; }; total: { value: number; text: string; }; variantGroupId?: number; removeApi: string; // API endpoint to remove item from cart updateQtyApi: string; // API endpoint to update item quantity errors?: string[]; // Validation errors for this item } export interface PaymentMethod { id: string; name: string; code: string; } export interface ShippingMethod { id: string; name: string; code: string; price: number; } export interface ShippingAddressParams { country: string; province?: string; postcode?: string; } export interface CartError { field?: string; message: string; code?: string; } // Extensible cart data interface - third-party extensions can add any fields from server-side cart data export interface CartData { uuid?: string; // Cart unique identifier currency: string; // Currency code totalQty: number; // Total quantity of items totalWeight: { value: number; unit: string; }; // Total weight of items customerId?: number; // Optional customer ID customerGroupId?: number; // Optional customer group ID customerEmail?: string; // Optional customer email customerFullName?: string; // Optional customer full name coupon?: string; // Coupon code applied to cart noShippingRequired: boolean; shippingMethod?: string; // Selected shipping method code shippingMethodName?: string; // Selected shipping method name paymentMethod?: string; // Selected payment method code paymentMethodName?: string; // Selected payment method name shippingNote?: string; // Shipping note items: CartItem[]; taxAmount: { value: number; text: string; }; totalTaxAmount: { value: number; text: string; }; taxAmountBeforeDiscount: { value: number; text: string; }; discountAmount: { value: number; text: string; }; shippingFeeExclTax: { value: number; text: string; }; shippingFeeInclTax: { value: number; text: string; }; shippingTaxAmount: { value: number; text: string; }; subTotal: { value: number; text: string; }; subTotalInclTax: { value: number; text: string; }; subTotalWithDiscount: { value: number; text: string; }; subTotalWithDiscountInclTax: { value: number; text: string; }; grandTotal: { value: number; text: string; }; billingAddress?: CustomerAddressGraphql; shippingAddress?: CustomerAddressGraphql; createdAt: { value: string; text: string; }; updatedAt: { value: string; text: string; }; // API endpoints addItemApi: string; addPaymentMethodApi: string; addShippingMethodApi: string; addContactInfoApi: string; addAddressApi: string; addNoteApi: string; applyCouponApi: string; checkoutApi: string; removeCouponApi?: string; // Available methods availablePaymentMethods: { code: string; name: string; }[]; availableShippingMethods: { code: string; name: string; cost?: { value: number; text: string; }; }[]; // Errors errors: CartError[]; error: string | null; [extendedFields: string]: unknown; // Allow third-party extensions to add fields } // Complete cart state with detailed loading states export interface CartState { data: CartData; // Cart data, can be undefined if not initialized loading: boolean; // Overall loading state (true if any operation is loading) - derived from loadingStates loadingStates: { addingItem: boolean; removingItem: string | null; // Item ID being removed, null if none updatingItem: string | null; // Item ID being updated, null if none addingPaymentMethod: boolean; addingShippingMethod: boolean; addingShippingAddress: boolean; addingBillingAddress: boolean; addingContactInfo: boolean; applyingCoupon: boolean; removingCoupon: boolean; fetchingShippingMethods: boolean; // New loading state for fetching shipping methods }; syncStatus: { syncing: boolean; // Whether a sync operation is in progress synced: boolean; // Whether the last sync was successful trigger?: string; // The reason/trigger that caused the sync (can be internal enum or external string) }; } // Action type for cart operations type CartAction = | { type: 'SET_CART'; payload: Partial } | { type: 'SET_SPECIFIC_LOADING'; payload: { operation: keyof CartState['loadingStates']; loading: boolean; itemId?: string; }; } | { type: 'SET_ERROR'; payload: string | null } | { type: 'CLEAR_ERROR' } | { type: 'SET_SYNC_STATUS'; payload: { syncing?: boolean; synced?: boolean; trigger?: string }; }; // The shape of the functions that will be returned by the useCartDispatch hook interface CartDispatch { addItem: (payload: { sku: string; qty: number }) => Promise; removeItem: (itemId: string) => Promise; updateItem: ( itemId: string, payload: { qty: number; action: 'increase' | 'decrease' } ) => Promise; addPaymentMethod: (code: string, name: string) => Promise; addShippingMethod: (code: string, name: string) => Promise; addShippingAddress: (address: Address) => Promise; addBillingAddress: (address: Address) => Promise; addContactInfo: (contactInfo: { email: string }) => Promise; applyCoupon: (couponCode: string) => Promise; removeCoupon: () => Promise; clearError: () => void; isShippingRequired: () => boolean; isReadyForCheckout: () => boolean; getErrors: () => CartError[]; getId: () => string | null; fetchAvailableShippingMethods: ( params: ShippingAddressParams ) => Promise; syncCartWithServer: (trigger?: string) => Promise; // Added trigger parameter } const cartReducer = (state: CartState, action: CartAction): CartState => { return produce(state, (draft) => { switch (action.type) { case 'SET_CART': if (draft.data) { Object.assign(draft.data, action.payload); draft.data.error = null; } else { draft.data = action.payload as CartData; } // Clear all loading states when cart is set draft.loadingStates = { addingItem: false, removingItem: null, updatingItem: null, addingPaymentMethod: false, addingShippingMethod: false, addingShippingAddress: false, addingBillingAddress: false, addingContactInfo: false, applyingCoupon: false, removingCoupon: false, fetchingShippingMethods: false }; draft.loading = false; break; case 'SET_SPECIFIC_LOADING': const { operation, loading, itemId } = action.payload; if (operation === 'removingItem' || operation === 'updatingItem') { draft.loadingStates[operation] = loading ? itemId || null : null; } else { (draft.loadingStates as any)[operation] = loading; } // Update overall loading state based on loadingStates draft.loading = Object.values(draft.loadingStates).some( (state) => state === true || (typeof state === 'string' && state !== null) ); break; case 'SET_ERROR': if (draft.data) { draft.data.error = action.payload; } // Clear all loading states on error draft.loadingStates = { addingItem: false, removingItem: null, updatingItem: null, addingPaymentMethod: false, addingShippingMethod: false, addingShippingAddress: false, addingBillingAddress: false, addingContactInfo: false, applyingCoupon: false, removingCoupon: false, fetchingShippingMethods: false }; draft.loading = false; break; case 'CLEAR_ERROR': if (draft.data) { draft.data.error = null; draft.data.errors = []; } break; case 'SET_SYNC_STATUS': Object.assign(draft.syncStatus, action.payload); break; } }); }; const CartStateContext = createContext(undefined); const CartDispatchContext = createContext(undefined); interface CartProviderProps { children: ReactNode; query: string; cart?: CartData; addMineCartItemApi: string; } const initialEmptyState: CartState = { data: { currency: 'USD', addItemApi: '', // initial addItemApi items: [], totalQty: 0, noShippingRequired: false, totalWeight: { value: 0, unit: 'kg' }, billingAddress: undefined, shippingAddress: undefined, errors: [], error: null, taxAmount: { value: 0, text: '0.00' }, totalTaxAmount: { value: 0, text: '0.00' }, taxAmountBeforeDiscount: { value: 0, text: '0.00' }, discountAmount: { value: 0, text: '0.00' }, shippingFeeExclTax: { value: 0, text: '0.00' }, shippingFeeInclTax: { value: 0, text: '0.00' }, shippingTaxAmount: { value: 0, text: '0.00' }, subTotal: { value: 0, text: '0.00' }, subTotalInclTax: { value: 0, text: '0.00' }, subTotalWithDiscount: { value: 0, text: '0.00' }, subTotalWithDiscountInclTax: { value: 0, text: '0.00' }, grandTotal: { value: 0, text: '0.00' }, createdAt: { value: '', text: '' }, updatedAt: { value: '', text: '' }, coupon: '', addPaymentMethodApi: '', // Will be set by server addShippingMethodApi: '', // Will be set by server addAddressApi: '', // Will be set by server applyCouponApi: '', // Will be set by server addNoteApi: '', // Will be set by server addContactInfoApi: '', // Will be set by server checkoutApi: '', // Will be set by server availablePaymentMethods: [], availableShippingMethods: [] }, loading: false, loadingStates: { addingItem: false, removingItem: null, updatingItem: null, addingPaymentMethod: false, addingShippingMethod: false, addingShippingAddress: false, addingBillingAddress: false, addingContactInfo: false, applyingCoupon: false, removingCoupon: false, fetchingShippingMethods: false }, syncStatus: { syncing: false, synced: false, trigger: undefined } }; export const CartProvider = ({ children, query, cart, addMineCartItemApi }: CartProviderProps) => { const client = useClient(); // Get urql client for GraphQL queries const hydratedInitialState: Partial = { loading: initialEmptyState.loading, loadingStates: { ...initialEmptyState.loadingStates }, syncStatus: { ...initialEmptyState.syncStatus } }; if (cart) { hydratedInitialState.data = cart; } else { hydratedInitialState.data = { ...initialEmptyState.data, addItemApi: addMineCartItemApi }; } const [state, dispatch] = useReducer(cartReducer, hydratedInitialState); // Use urql to query cart data const [cartQueryResult, refetchCart] = useQuery({ query: query, pause: true }); const retry = async function ( fn: () => Promise, retries = 2, delay = 1000 ): Promise { try { return await fn(); } catch (error) { if (retries > 0) { await new Promise((resolve) => setTimeout(resolve, delay)); return retry(fn, retries - 1, delay * 2); } throw error; } }; const syncCartWithServer = useCallback( async (trigger?: string): Promise => { try { // Set syncing to true and synced to false when starting sync dispatch({ type: 'SET_SYNC_STATUS', payload: { syncing: true, synced: false, trigger } }); await refetchCart({ requestPolicy: 'network-only' }); // Set syncing to false and synced to true on success dispatch({ type: 'SET_SYNC_STATUS', payload: { syncing: false, synced: true, trigger } }); } catch (error) { dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to sync cart' }); // Set syncing to false and keep synced as false on error dispatch({ type: 'SET_SYNC_STATUS', payload: { syncing: false, synced: false, trigger } }); } }, [refetchCart] ); // Effect to update cart when GraphQL query result changes React.useEffect(() => { // Only process if we have fetched data (either successful or error state) if (cartQueryResult.fetching === false) { if (cartQueryResult.data?.myCart) { const serverCart = cartQueryResult.data.myCart; dispatch({ type: 'SET_CART', payload: serverCart }); } else if (cartQueryResult.error) { // Handle error case dispatch({ type: 'SET_ERROR', payload: cartQueryResult.error.message || 'Failed to fetch cart data' }); } else if (cartQueryResult.operation) { // Query executed but returned no data - initialize empty cart dispatch({ type: 'SET_CART', payload: { ...initialEmptyState.data, addItemApi: addMineCartItemApi } }); } } }, [ cartQueryResult.data, cartQueryResult.error, cartQueryResult.fetching, cartQueryResult.operation ]); React.useEffect(() => { if (cart && JSON.stringify(cart) !== JSON.stringify(state.data)) { dispatch({ type: 'SET_CART', payload: cart }); } }, [cart]); const addItem = useCallback( async (payload: { sku: string; qty: number }) => { if (!state.data) { throw new Error('Cannot add item: cart not initialized'); } try { // Set specific loading state dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'addingItem', loading: true } }); // Server request with retry const response = await retry(() => fetch(state.data!.addItemApi, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) ); const json = (await response.json()) as ApiResponse; if (!response.ok) { throw new Error(json.error?.message || 'Failed to add item.'); } // Sync with server (both immediate update and GraphQL refetch) await syncCartWithServer(CartSyncTrigger.ADD_ITEM); } catch (error) { dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to add item' }); throw error; } finally { dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'addingItem', loading: false } }); } }, [state.data?.addItemApi, syncCartWithServer] ); const removeItem = useCallback( async (itemId: string) => { if (!state.data) { throw new Error('Cannot remove item: cart not initialized'); } const item = state.data.items.find((item) => item.cartItemId === itemId); if (!item) { throw new Error('Item not found in cart'); } try { // Set specific loading state for this item dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'removingItem', loading: true, itemId } }); // Server request with retry using item's remove API const response = await retry(() => fetch(item.removeApi, { method: 'DELETE' }) ); const json = await response.json(); if (!response.ok) { throw new Error(json.error?.message || 'Failed to remove item.'); } // Sync with server (both immediate update and GraphQL refetch) await syncCartWithServer(CartSyncTrigger.REMOVE_ITEM); } catch (error) { dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to remove item' }); throw error; } finally { dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'removingItem', loading: false } }); } }, [state, syncCartWithServer] ); const updateItem = useCallback( async ( itemId: string, payload: { qty: number; action: 'increase' | 'decrease' } ) => { if (!state.data) { throw new Error('Cannot update item: cart not initialized'); } const item = state.data.items.find((item) => item.cartItemId === itemId); if (!item) { throw new Error('Item not found in cart'); } try { // Set specific loading state for this item dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'updatingItem', loading: true, itemId } }); // Server request with retry using item's update API const response = await retry(() => fetch(item.updateQtyApi, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) ); const json = await response.json(); if (!response.ok) { throw new Error(json.error?.message || 'Failed to update item.'); } // Sync with server (both immediate update and GraphQL refetch) await syncCartWithServer(CartSyncTrigger.UPDATE_ITEM); } catch (error) { dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to update item' }); throw error; } finally { dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'updatingItem', loading: false } }); } }, [state, syncCartWithServer] ); // Clear error function const clearError = useCallback(() => { dispatch({ type: 'CLEAR_ERROR' }); }, []); // Add payment method const addPaymentMethod = useCallback( async (code: string, name: string) => { if (!state.data) { throw new Error(_('Cannot add payment method: cart not initialized')); } try { dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'addingPaymentMethod', loading: true } }); const response = await retry(() => fetch(state.data!.addPaymentMethodApi, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ method_code: code, method_name: name }) }) ); const json = await response.json(); if (!response.ok) { throw new Error( json.error?.message || _('Failed to add payment method.') ); } // Sync with server (both immediate update and GraphQL refetch) await syncCartWithServer(CartSyncTrigger.ADD_PAYMENT_METHOD); } catch (error) { dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : _('Failed to add payment method') }); throw error; } finally { dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'addingPaymentMethod', loading: false } }); } }, [state.data?.addPaymentMethodApi, syncCartWithServer] ); // Add shipping method const addShippingMethod = useCallback( async (code: string, name: string) => { if (!state.data) { throw new Error(_('Cannot add shipping method: cart not initialized')); } try { dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'addingShippingMethod', loading: true } }); const response = await retry(() => fetch(state.data!.addShippingMethodApi, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ method_code: code, method_name: name }) }) ); const json = await response.json(); if (!response.ok) { throw new Error( json.error?.message || _('Failed to add shipping method.') ); } // Sync with server (both immediate update and GraphQL refetch) await syncCartWithServer(CartSyncTrigger.ADD_SHIPPING_METHOD); } catch (error) { dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : _('Failed to add shipping method') }); throw error; } finally { dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'addingShippingMethod', loading: false } }); } }, [state.data?.addShippingMethodApi, syncCartWithServer] ); // Add shipping address const addShippingAddress = useCallback( async (address: Address) => { if (!state.data) { throw new Error(_('Cannot add shipping address: cart not initialized')); } try { dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'addingShippingAddress', loading: true } }); const response = await retry(() => fetch(state.data!.addAddressApi, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: { ...address }, type: 'shipping' }) }) ); const json = await response.json(); if (!response.ok) { throw new Error( json.error?.message || _('Failed to add shipping address.') ); } // Sync with server (both immediate update and GraphQL refetch) await syncCartWithServer(CartSyncTrigger.ADD_SHIPPING_ADDRESS); } catch (error) { dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : _('Failed to add shipping address') }); throw error; } finally { dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'addingShippingAddress', loading: false } }); } }, [state.data?.addAddressApi, syncCartWithServer] ); // Add billing address const addBillingAddress = useCallback( async (address: Address) => { if (!state.data) { throw new Error(_('Cannot add billing address: cart not initialized')); } try { dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'addingBillingAddress', loading: true } }); const response = await retry(() => fetch(state.data!.addAddressApi, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: { ...address }, type: 'billing' }) }) ); const json = await response.json(); if (!response.ok) { throw new Error( json.error?.message || _('Failed to add billing address.') ); } // Sync with server (both immediate update and GraphQL refetch) await syncCartWithServer(CartSyncTrigger.ADD_BILLING_ADDRESS); } catch (error) { dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : _('Failed to add billing address') }); throw error; } finally { dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'addingBillingAddress', loading: false } }); } }, [state.data?.addAddressApi, syncCartWithServer] ); // Add contact info const addContactInfo = useCallback( async (contactInfo: { email: string }) => { if (!state.data) { throw new Error(_('Cannot add contact info: cart not initialized')); } try { dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'addingContactInfo', loading: true } }); const response = await retry(() => fetch(state.data!.addContactInfoApi, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(contactInfo) }) ); const json = await response.json(); if (!response.ok) { throw new Error( json.error?.message || _('Failed to add contact info.') ); } // Sync with server (both immediate update and GraphQL refetch) await syncCartWithServer(CartSyncTrigger.ADD_CONTACT_INFO); } catch (error) { dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : _('Failed to add contact info') }); throw error; } finally { dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'addingContactInfo', loading: false } }); } }, [state.data?.addContactInfoApi, syncCartWithServer] ); // Apply coupon const applyCoupon = useCallback( async (couponCode: string) => { if (!state.data) { throw new Error(_('Cannot apply coupon: cart not initialized')); } try { dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'applyingCoupon', loading: true } }); const response = await retry(() => fetch(state.data!.applyCouponApi, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ coupon: couponCode }) }) ); const json = await response.json(); if (!response.ok) { throw new Error(json.error?.message || 'Failed to apply coupon.'); } // Sync with server (both immediate update and GraphQL refetch) await syncCartWithServer(CartSyncTrigger.APPLY_COUPON); } catch (error) { dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to apply coupon' }); throw error; } finally { dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'applyingCoupon', loading: false } }); } }, [state.data?.applyCouponApi, syncCartWithServer] ); // Remove coupon const removeCoupon = useCallback(async () => { if (!state.data) { throw new Error(_('Cannot remove coupon: cart not initialized')); } if (!state.data?.removeCouponApi) { throw new Error(_('No coupon to remove')); } try { dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'removingCoupon', loading: true } }); const response = await retry(() => fetch(state.data!.removeCouponApi as string, { method: 'DELETE', headers: { 'Content-Type': 'application/json' } }) ); const json = await response.json(); if (!response.ok) { throw new Error(json.error?.message || _('Failed to remove coupon.')); } // Sync with server (both immediate update and GraphQL refetch) await syncCartWithServer(CartSyncTrigger.REMOVE_COUPON); } catch (error) { dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : _('Failed to remove coupon') }); throw error; } finally { dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'removingCoupon', loading: false } }); } }, [state.data?.removeCouponApi, syncCartWithServer]); // Check if shipping is required // Note: Currently assumes all items require shipping // If you need to support virtual/downloadable products, add a 'virtual' or 'requiresShipping' field to CartItem const isShippingRequired = useCallback(() => { if (!state.data) return false; // If there are items in the cart, shipping is required // This can be enhanced with a virtual/downloadable product check if needed return state.data.items.length > 0; }, [state.data?.items]); // Check if cart is ready for checkout const isReadyForCheckout = useCallback(() => { if (!state.data) return false; const hasItems = state.data.items.length > 0; const hasBillingAddress = !!state.data.billingAddress; const hasShippingAddress = !isShippingRequired() || !!state.data.shippingAddress; const noErrors = state.data.errors.length === 0; return hasItems && hasBillingAddress && hasShippingAddress && noErrors; }, [state.data, isShippingRequired]); // Get validation errors const getErrors = useCallback(() => { return state.data?.errors ?? []; }, [state.data?.errors]); // Get cart ID const getId = useCallback(() => { return state.data?.uuid ?? null; }, [state.data?.uuid]); // Fetch available shipping methods based on address parameters and update cart state const fetchAvailableShippingMethods = useCallback( async (params: ShippingAddressParams) => { if (!state.data?.uuid) { throw new Error('Cannot fetch shipping methods: cart not initialized'); } try { dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'fetchingShippingMethods', loading: true } }); const result = await client .query(ShippingMethodsQuery, { country: params.country, province: params.province || null, postcode: params.postcode || null }) .toPromise(); if (result.error) { throw new Error( result.error.message || 'Failed to fetch shipping methods' ); } // Update cart state with new shipping methods if (result.data?.myCart?.availableShippingMethods) { dispatch({ type: 'SET_CART', payload: { availableShippingMethods: result.data.myCart.availableShippingMethods } }); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Failed to fetch shipping methods'; dispatch({ type: 'SET_ERROR', payload: errorMessage }); throw new Error(errorMessage); } finally { dispatch({ type: 'SET_SPECIFIC_LOADING', payload: { operation: 'fetchingShippingMethods', loading: false } }); } }, [state.data?.uuid, client] ); const cartDispatch: CartDispatch = { addItem, removeItem, updateItem, addPaymentMethod, addShippingMethod, addShippingAddress, addBillingAddress, addContactInfo, applyCoupon, removeCoupon, clearError, isShippingRequired, isReadyForCheckout, getErrors, getId, fetchAvailableShippingMethods, syncCartWithServer }; return ( {children} ); }; export const useCartState = (): CartState => { const context = useContext(CartStateContext); if (!context) { throw new Error('useCartState must be used within a CartProvider'); } return context; }; export const useCartDispatch = (): CartDispatch => { const context = useContext(CartDispatchContext); if (!context) { throw new Error('useCartDispatch must be used within a CartProvider'); } return context; }; ================================================ FILE: packages/evershop/src/components/frontStore/cart/CartItems.tsx ================================================ import Area from '@components/common/Area.js'; import { useAppState } from '@components/common/context/app.js'; import { useCartState, useCartDispatch, CartItem } from '@components/frontStore/cart/CartContext.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; interface CartItemsProps { children: (props: { items: CartItem[]; showPriceIncludingTax?: boolean; loading: boolean; isEmpty: boolean; totalItems: number; onRemoveItem: (itemId: string) => Promise; }) => React.ReactNode; } function CartItems({ children }: CartItemsProps) { const { data: cart, loading } = useCartState(); const { config: { tax: { priceIncludingTax } } } = useAppState(); const { removeItem } = useCartDispatch(); const isEmpty = cart?.totalQty === 0; const totalItems = cart?.totalQty || 0; const handleRemoveItem = async (itemId: string) => { await removeItem(itemId); }; return (
    {children ? children({ items: cart?.items || [], showPriceIncludingTax: priceIncludingTax, loading, isEmpty, totalItems, onRemoveItem: handleRemoveItem }) : null}
    ); } export { CartItems }; ================================================ FILE: packages/evershop/src/components/frontStore/cart/CartSummaryItems.tsx ================================================ import { Image } from '@components/common/Image.js'; import { ProductNoThumbnail } from '@components/common/ProductNoThumbnail.js'; import { Skeleton } from '@components/common/ui/Skeleton.js'; import { CartItem } from '@components/frontStore/cart/CartContext.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; const CartSummarySkeleton: React.FC<{ rows?: number }> = ({ rows = 2 }) => { return (
      {Array.from({ length: rows }).map((_, i) => (
    • {i + 1}
    • ))}
    ); }; const CartSummaryItemsList: React.FC<{ items: CartItem[]; loading: boolean; showPriceIncludingTax?: boolean; }> = ({ items, loading, showPriceIncludingTax }) => { if (loading) { return ; } if (items.length === 0) { return (

    {_('Your cart is empty')}

    {_('Add some items to get started')}

    ); } return (
      {items.map((item) => (
    • {item.thumbnail && ( {item.productName} )} {!item.thumbnail && ( )} {item.qty}
      {item.productName}
      {item.variantOptions && item.variantOptions.length > 0 && (
      {item.variantOptions.map((option) => (
      {option.attributeName}:{' '} {option.optionText}
      ))}
      )}
      {showPriceIncludingTax ? item.lineTotalInclTax.text : item.lineTotal.text}
    • ))}
    ); }; export { CartSummaryItemsList }; ================================================ FILE: packages/evershop/src/components/frontStore/cart/CartTotalSummary.tsx ================================================ import Area from '@components/common/Area.js'; import { useAppState } from '@components/common/context/app.js'; import { Skeleton } from '@components/common/ui/Skeleton.js'; import { useCartState } from '@components/frontStore/cart/CartContext.js'; import { Coupon, CouponState, CouponActions } from '@components/frontStore/Coupon.js'; import { CouponForm } from '@components/frontStore/CouponForm.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import { CircleX } from 'lucide-react'; import React from 'react'; const SkeletonValue: React.FC<{ children: React.ReactNode; loading?: boolean; className?: string; }> = ({ children, loading = false, className = '' }) => { if (!loading) { return <>{children}; } return ( {children} ); }; const Total: React.FC<{ total: string; totalTaxAmount: string; priceIncludingTax: boolean; loading?: boolean; }> = ({ total, totalTaxAmount, priceIncludingTax, loading = false }) => { return (
    {(priceIncludingTax && (
    {_('Total')}
    ({_('Inclusive of tax ${totalTaxAmount}', { totalTaxAmount })})
    )) || {_('Total')}}
    {total}
    ); }; const Tax: React.FC<{ showPriceIncludingTax: boolean; amount: string; loading?: boolean; }> = ({ showPriceIncludingTax, amount, loading = false }) => { if (showPriceIncludingTax) { return null; } return (
    {_('Tax')}
    {amount}
    ); }; const Subtotal: React.FC<{ subTotal: string; loading?: boolean }> = ({ subTotal, loading = false }) => { return (
    {_('Sub total')}
    {subTotal}
    ); }; const Discount: React.FC<{ discountAmount: string; coupon: string | undefined; loading?: boolean; }> = ({ discountAmount, coupon, loading = false }) => { if (!coupon) { return (
    ); } return (
    {(state: CouponState, actions: CouponActions) => ( <>
    {_('Discount(${coupon})', { coupon })} {!state.isLoading && ( { e.preventDefault(); await actions.removeCoupon(); }} > )}
    {discountAmount} )}
    ); }; const Shipping: React.FC<{ method: string | undefined; cost: string | undefined; noShippingRequired: boolean; loading?: boolean; }> = ({ method, cost, noShippingRequired, loading = false }) => { return (
    {noShippingRequired && ( <> {_('Shipping')} {_('No shipping required')} )} {method && !noShippingRequired && ( <> {_('Shipping (${method})', { method })}
    {cost}
    )} {!method && !noShippingRequired && ( <> {_('Shipping')} {_('Select shipping method')} )}
    ); }; const DefaultCartSummary: React.FC<{ loading: boolean; showPriceIncludingTax: boolean; noShippingRequired: boolean; subTotal: string; discountAmount: string; coupon: string | undefined; shippingMethod: string | undefined; shippingCost: string | undefined; taxAmount: string; total: string; }> = ({ loading, showPriceIncludingTax, noShippingRequired, subTotal, discountAmount, coupon, shippingMethod, shippingCost, taxAmount, total }) => (
    ); interface CartTotalSummaryProps { children?: (props: { loading: boolean; showPriceIncludingTax: boolean; noShippingRequired: boolean; subTotal: string; discountAmount: string; coupon: string | undefined; shippingMethod: string | undefined; shippingCost: string | undefined; taxAmount: string; total: string; }) => React.ReactNode; } function CartTotalSummary({ children }: CartTotalSummaryProps) { const { data: cart, loadingStates } = useCartState(); const { config: { tax: { priceIncludingTax } } } = useAppState(); const subTotal = priceIncludingTax ? cart?.subTotalInclTax?.text || '' : cart?.subTotal?.text || ''; const discountAmount = cart?.discountAmount?.text || ''; const coupon = cart?.coupon; const shippingMethod = cart?.shippingMethodName; const shippingCost = priceIncludingTax ? cart?.shippingFeeInclTax?.text || '' : cart?.shippingFeeExclTax?.text || ''; const taxAmount = cart?.totalTaxAmount?.text || ''; const total = cart?.grandTotal?.text || ''; return (
    {children ? ( children({ loading: Object.values(loadingStates).some( (state) => state === true || (typeof state === 'string' && state !== null) ), showPriceIncludingTax: priceIncludingTax, noShippingRequired: cart?.noShippingRequired || false, subTotal, discountAmount, coupon, shippingMethod, shippingCost, taxAmount, total }) ) : ( state === true || (typeof state === 'string' && state !== null) )} showPriceIncludingTax={priceIncludingTax} noShippingRequired={cart?.noShippingRequired || false} subTotal={subTotal} discountAmount={discountAmount} coupon={coupon} shippingMethod={shippingMethod} shippingCost={shippingCost} taxAmount={taxAmount} total={total} /> )}
    ); } export { CartTotalSummary, DefaultCartSummary, Subtotal, Discount, Shipping, Tax, Total }; ================================================ FILE: packages/evershop/src/components/frontStore/cart/DefaultCartItemList.tsx ================================================ import { Area } from '@components/common/Area.js'; import { ExtendableTable, TableColumn } from '@components/common/ExtendableTable.js'; import { Image } from '@components/common/Image.js'; import { ProductNoThumbnail } from '@components/common/ProductNoThumbnail.js'; import { CartItem } from '@components/frontStore/cart/CartContext.js'; import { ItemQuantity } from '@components/frontStore/cart/ItemQuantity.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; interface CartItemsTableProps { items: CartItem[]; showPriceIncludingTax?: boolean; loading?: boolean; onSort?: (key: string, direction: 'asc' | 'desc') => void; currentSort?: { key: string; direction: 'asc' | 'desc' }; onRemoveItem?: (itemId: string) => Promise; } export const DefaultCartItemList: React.FC = ({ items, showPriceIncludingTax = true, loading = false, onSort, currentSort, onRemoveItem }) => { const columns: TableColumn[] = [ { key: 'productInfo', header: { label: _('Product'), className: '' }, className: 'font-medium align-top', sortable: false, render: (row) => { const priceValue = showPriceIncludingTax ? row.productPriceInclTax?.text : row.productPrice?.text; return (
    {row.thumbnail ? ( {row.productName} ) : ( )}
    {row.productName}
    {row.variantOptions?.map((option) => ( {option.attributeName}:{' '} {option.optionText} ))} {priceValue} x {row.qty} { e.preventDefault(); onRemoveItem?.(row.cartItemId); }} > {_('Remove')} {row.errors?.map((error, index) => ( {error} ))}
    ); } }, { key: 'qty', header: { label: _('Quantity'), className: 'text-right' }, sortable: true, render: (row) => { return (
    {({ quantity, increase, decrease }) => (
    {quantity}
    )}
    ); } }, { key: 'lineTotal', header: { label: _('Total'), className: 'text-right' }, sortable: true, render: (row) => { const totalValue = showPriceIncludingTax ? row.lineTotalInclTax?.text : row.lineTotal?.text; return (
    {totalValue}
    ); } } ]; const [rows, setRows] = React.useState(items); React.useEffect(() => { setRows(items); }, [items]); return ( <> ); }; ================================================ FILE: packages/evershop/src/components/frontStore/cart/DefaultMiniCartDropdown.tsx ================================================ import { Area } from '@components/common/Area.js'; import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@components/common/ui/Sheet.js'; import { CartData } from '@components/frontStore/cart/CartContext.js'; import { CartItems } from '@components/frontStore/cart/CartItems.js'; import { CartTotalSummary } from '@components/frontStore/cart/CartTotalSummary.js'; import { DefaultMiniCartDropdownEmpty } from '@components/frontStore/cart/DefaultMiniCartDropdownEmpty.js'; import { DefaultMiniCartDropdownSummary } from '@components/frontStore/cart/DefaultMinicartDropdownSummary.js'; import { DefaultMiniCartItemList } from '@components/frontStore/cart/DefaultMiniCartItemList.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; export const DefaultMiniCartDropdown: React.FC<{ cart: CartData | null; isOpen: boolean; onClose: () => void; cartUrl?: string; checkoutUrl?: string; dropdownPosition?: 'left' | 'right'; setIsDropdownOpen: (isOpen: boolean) => void; }> = ({ cart, isOpen, onClose, cartUrl, checkoutUrl, dropdownPosition = 'right', setIsDropdownOpen }) => { const totalQty = cart?.totalQty || 0; return ( !open && onClose()}> {_('Your Cart')} {totalQty === 0 ? ( ) : (
    {({ items, loading }) => ( )}
    {({ total }) => ( )}
    )}
    ); }; ================================================ FILE: packages/evershop/src/components/frontStore/cart/DefaultMiniCartDropdownEmpty.tsx ================================================ import { Area } from '@components/common/Area.js'; import { Button } from '@components/common/ui/Button.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import { ShoppingBag } from 'lucide-react'; import React from 'react'; export const DefaultMiniCartDropdownEmpty: React.FC<{ setIsDropdownOpen: (isOpen: boolean) => void; }> = ({ setIsDropdownOpen }) => (

    {_('Your cart is empty')}

    ); ================================================ FILE: packages/evershop/src/components/frontStore/cart/DefaultMiniCartIcon.tsx ================================================ import { ShoppingBag } from 'lucide-react'; import React from 'react'; export const DefaultMiniCartIcon = ({ totalQty, onClick, isOpen, disabled = false, showItemCount = true, syncStatus }: { totalQty: number; onClick: () => void; isOpen: boolean; disabled?: boolean; showItemCount?: boolean; syncStatus: { syncing: boolean }; }) => { return ( ); }; ================================================ FILE: packages/evershop/src/components/frontStore/cart/DefaultMiniCartItemList.tsx ================================================ import { CartItem } from '@components/frontStore/cart/CartContext.js'; import { CartSummaryItemsList } from '@components/frontStore/cart/CartSummaryItems.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; interface CartItemsTableProps { items: CartItem[]; showPriceIncludingTax?: boolean; loading?: boolean; onSort?: (key: string, direction: 'asc' | 'desc') => void; currentSort?: { key: string; direction: 'asc' | 'desc' }; } export const DefaultMiniCartItemList: React.FC = ({ items, showPriceIncludingTax = true, loading = false }) => { return ( ); }; ================================================ FILE: packages/evershop/src/components/frontStore/cart/DefaultMinicartDropdownSummary.tsx ================================================ import Area from '@components/common/Area.js'; import { Button } from '@components/common/ui/Button.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; export function DefaultMiniCartDropdownSummary({ total, cartUrl, checkoutUrl, totalQty }: { total: string; cartUrl: string; checkoutUrl: string; totalQty: number; }) { return ( <>
    {_('Subtotal')}: {total || '—'}
    ); } ================================================ FILE: packages/evershop/src/components/frontStore/cart/ItemQuantity.tsx ================================================ import { useCartDispatch, useCartState } from '@components/frontStore/cart/CartContext.js'; import React, { useState, useCallback, useEffect, ReactNode, useRef } from 'react'; interface UseItemQuantityProps { initialValue?: number; min?: number; max?: number; onChange?: (quantity: number) => void; cartItemId: string; debounce?: number; onSuccess?: () => void; onFailure?: (error: Error) => void; } interface UseItemQuantityReturn { quantity: number; loading: boolean; increase: () => void; decrease: () => void; setQuantity: (quantity: number) => void; inputProps: { value: number; onChange: (e: React.ChangeEvent) => void; onBlur: () => void; type: 'number'; min?: number; max?: number; disabled: boolean; }; } export const useItemQuantity = ({ initialValue = 1, min = 1, max = Infinity, onChange, cartItemId, debounce = 500, onSuccess, onFailure }: UseItemQuantityProps): UseItemQuantityReturn => { const [quantity, setInternalQuantity] = useState(initialValue); const cartDispatch = useCartDispatch(); const { loading } = useCartState(); const previousQuantityRef = useRef(initialValue); const debounceTimerRef = useRef(null); useEffect(() => { setInternalQuantity(initialValue); previousQuantityRef.current = initialValue; }, [initialValue]); const executeCartUpdate = useCallback( async (newQuantity: number) => { try { const currentQuantity = previousQuantityRef.current; const diff = newQuantity - currentQuantity; if (diff === 0) { return; } const action = diff > 0 ? 'increase' : 'decrease'; const qty = Math.abs(diff); await cartDispatch.updateItem(cartItemId, { qty, action }); previousQuantityRef.current = newQuantity; if (onChange) { onChange(newQuantity); } if (onSuccess) { onSuccess(); } } catch (error) { // Revert to the last known good state on failure setInternalQuantity(previousQuantityRef.current); if (onFailure) { onFailure(error as Error); } } }, [cartItemId, cartDispatch, onChange, onSuccess, onFailure] ); const handleUpdate = useCallback( (newQuantity: number, immediate = false) => { const clampedQuantity = Math.max(min, Math.min(newQuantity, max)); setInternalQuantity(clampedQuantity); if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } if (clampedQuantity === previousQuantityRef.current) { return; } if (immediate || debounce === 0) { executeCartUpdate(clampedQuantity); } else { debounceTimerRef.current = setTimeout(() => { executeCartUpdate(clampedQuantity); }, debounce); } }, [min, max, quantity, debounce, executeCartUpdate] ); const increase = useCallback(() => { handleUpdate(quantity + 1); }, [quantity, handleUpdate]); const decrease = useCallback(() => { handleUpdate(quantity - 1); }, [quantity, handleUpdate]); const setQuantity = useCallback( (newQuantity: number) => { handleUpdate(newQuantity); }, [handleUpdate] ); const handleInputChange = (e: React.ChangeEvent) => { const newQuantity = parseInt(e.target.value, 10); if (!isNaN(newQuantity)) { setInternalQuantity(newQuantity); } else if (e.target.value === '') { setInternalQuantity(0); } }; const handleInputBlur = () => { handleUpdate(quantity, true); }; return { quantity, loading, increase, decrease, setQuantity, inputProps: { value: quantity, onChange: handleInputChange, onBlur: handleInputBlur, type: 'number', min, max, disabled: loading } }; }; interface ItemQuantityProps extends UseItemQuantityProps { children: (props: UseItemQuantityReturn) => ReactNode; } /** * Item Quantity Component. * @param {ItemQuantityProps} props * @returns {ReactNode} */ export function ItemQuantity({ children, ...props }: ItemQuantityProps): ReactNode { const quantityControls = useItemQuantity(props); return children(quantityControls); } ================================================ FILE: packages/evershop/src/components/frontStore/cart/MiniCart.tsx ================================================ import { useCartState, CartData, CartSyncTrigger } from '@components/frontStore/cart/CartContext.js'; import { DefaultMiniCartDropdown } from '@components/frontStore/cart/DefaultMiniCartDropdown.js'; import { DefaultMiniCartIcon } from '@components/frontStore/cart/DefaultMiniCartIcon.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React, { useCallback, useState, useEffect } from 'react'; interface MiniCartProps { cartUrl?: string; checkoutUrl?: string; dropdownPosition?: 'left' | 'right'; showItemCount?: boolean; CartIconComponent?: React.FC<{ totalQty: number; onClick: () => void; isOpen: boolean; disabled?: boolean; showItemCount?: boolean; syncStatus: { syncing: boolean }; }>; CartDropdownComponent?: React.FC<{ cart: CartData | null; dropdownPosition?: 'left' | 'right'; onClose: () => void; cartUrl?: string; setIsDropdownOpen: (isOpen: boolean) => void; }>; onItemRemove?: (itemId: string) => Promise | void; className?: string; disabled?: boolean; } export function MiniCart({ cartUrl = '/cart', checkoutUrl = '/checkout', dropdownPosition = 'right', showItemCount = true, CartIconComponent, CartDropdownComponent, className = '', disabled = false }: MiniCartProps) { const { data: cartData, syncStatus } = useCartState(); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const cart = cartData; const handleCartClick = useCallback(() => { if (disabled) return; setIsDropdownOpen(!isDropdownOpen); }, [disabled, isDropdownOpen, cartUrl]); const handleDropdownClose = useCallback(() => { setIsDropdownOpen(false); }, []); useEffect(() => { if (syncStatus.synced && syncStatus.trigger === CartSyncTrigger.ADD_ITEM) { setIsDropdownOpen(true); } }, [syncStatus.synced, syncStatus.trigger]); return (
    {CartIconComponent ? ( ) : ( )} {CartDropdownComponent ? ( ) : ( )}
    ); } ================================================ FILE: packages/evershop/src/components/frontStore/cart/ShoppingCartEmpty.tsx ================================================ import { Button } from '@components/common/ui/Button.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; export function ShoppingCartEmpty() { return (

    {_('Shopping cart')}

    {_('Your cart is empty!')}
    ); } ================================================ FILE: packages/evershop/src/components/frontStore/catalog/CategoryContext.tsx ================================================ import { Row } from '@components/common/form/Editor.js'; import { ProductData } from '@components/frontStore/catalog/ProductContext.js'; import { CategoryFilter, FilterableAttribute, FilterInput } from '@components/frontStore/catalog/ProductFilter.js'; import React, { createContext, useContext, ReactNode } from 'react'; export interface CategoryProducts { items: ProductData[]; currentFilters: FilterInput[]; total: number; } export interface CategoryData { categoryId: number; uuid: string; name: string; description: Array; url?: string; image?: { alt: string; url: string; }; showProducts: boolean; products: CategoryProducts; availableAttributes: FilterableAttribute[]; children: CategoryFilter[]; priceRange: { min: number; minText: string; max: number; maxText: string; }; [extendedFields: string]: any; } const CategoryContext = createContext(undefined); interface CategoryProviderProps { children: ReactNode; category: CategoryData; } export const CategoryProvider: React.FC = ({ children, category }) => { return ( {children} ); }; export const useCategory = (): CategoryData => { const context = useContext(CategoryContext); if (context === undefined) { throw new Error('useCategory must be used within a CategoryProvider'); } return context; }; ================================================ FILE: packages/evershop/src/components/frontStore/catalog/CategoryInfo.tsx ================================================ import Area from '@components/common/Area.js'; import { Editor } from '@components/common/Editor.js'; import { Image } from '@components/common/Image.js'; import { useCategory } from '@components/frontStore/catalog/CategoryContext.js'; import React from 'react'; export function CategoryInfo() { const { name, description, image } = useCategory(); return ( <>
    {image && ( {image.alt )}

    {name}

    ); } ================================================ FILE: packages/evershop/src/components/frontStore/catalog/CategoryProducts.tsx ================================================ import Area from '@components/common/Area.js'; import { useCategory } from '@components/frontStore/catalog/CategoryContext.js'; import { ProductList } from '@components/frontStore/catalog/ProductList.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; export function CategoryProducts() { const { showProducts, products } = useCategory(); if (!showProducts) { return null; } return ( <>
    {_('${count} products', { count: products.total.toString() })}
    ); } ================================================ FILE: packages/evershop/src/components/frontStore/catalog/CategoryProductsFilter.tsx ================================================ import Area from '@components/common/Area.js'; import { useCategory } from '@components/frontStore/catalog/CategoryContext.js'; import { DefaultProductFilterRender } from '@components/frontStore/catalog/DefaultProductFilterRender.js'; import { ProductFilter } from '@components/frontStore/catalog/ProductFilter.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; export function CategoryProductsFilter() { const category = useCategory(); return ( <> {(renderProps) => ( )} ); } ================================================ FILE: packages/evershop/src/components/frontStore/catalog/CategoryProductsPagination.tsx ================================================ import { useCategory } from '@components/frontStore/catalog/CategoryContext.js'; import { Pagination, DefaultPaginationRenderer } from '@components/frontStore/Pagination.js'; import React from 'react'; export function CategoryProductsPagination() { const { showProducts, products } = useCategory(); if (!showProducts) { return null; } const page = products.currentFilters.find((filter) => filter.key === 'page'); const limit = products.currentFilters.find( (filter) => filter.key === 'limit' ); return ( {(paginationProps) => ( )} ); } ================================================ FILE: packages/evershop/src/components/frontStore/catalog/DefaultAttributeFilterRender.tsx ================================================ import { Button } from '@components/common/ui/Button.js'; import { Checkbox } from '@components/common/ui/Checkbox.js'; import { Label } from '@components/common/ui/Label.js'; import { FilterableAttribute, FilterInput, useProductFilter } from '@components/frontStore/catalog/ProductFilter.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React, { useState } from 'react'; export const DefaultAttributeFilterRender: React.FC<{ availableAttributes: FilterableAttribute[]; currentFilters: FilterInput[]; }> = ({ availableAttributes, currentFilters }) => { const { updateFilter } = useProductFilter(); const [searchTerms, setSearchTerms] = useState<{ [key: string]: string }>({}); const [collapsedAttributes, setCollapsedAttributes] = useState<{ [key: string]: boolean; }>({}); const handleAttributeChange = ( attributeCode: string, optionId: string, checked: boolean ) => { let newFilters = [...currentFilters]; const existingFilterIndex = newFilters.findIndex( (f) => f.key === attributeCode ); if (checked) { if (existingFilterIndex !== -1) { const existingFilter = newFilters[existingFilterIndex]; const values = existingFilter.value.split(','); if (!values.includes(optionId)) { values.push(optionId); newFilters[existingFilterIndex] = { ...existingFilter, value: values.join(',') }; } } else { newFilters.push({ key: attributeCode, operation: 'in', value: optionId }); } } else if (existingFilterIndex !== -1) { const existingFilter = newFilters[existingFilterIndex]; const values = existingFilter.value .split(',') .filter((v) => v !== optionId); if (values.length === 0) { newFilters = newFilters.filter((f) => f.key !== attributeCode); } else { newFilters[existingFilterIndex] = { ...existingFilter, value: values.join(',') }; } } updateFilter(newFilters); }; const isOptionSelected = (attributeCode: string, optionId: string) => { const filter = currentFilters.find((f) => f.key === attributeCode); return filter ? filter.value.split(',').includes(optionId.toString()) : false; }; const getSelectedCount = (attributeCode: string) => { const filter = currentFilters.find((f) => f.key === attributeCode); return filter ? filter.value.split(',').length : 0; }; const getFilteredOptions = (attribute: FilterableAttribute) => { const searchTerm = searchTerms[attribute.attributeCode] || ''; if (!searchTerm) return attribute.options; return attribute.options.filter((option) => option.optionText.toLowerCase().includes(searchTerm.toLowerCase()) ); }; const toggleCollapse = (attributeCode: string) => { setCollapsedAttributes((prev) => ({ ...prev, [attributeCode]: !prev[attributeCode] })); }; const clearAttributeFilter = (attributeCode: string) => { const newFilters = currentFilters.filter((f) => f.key !== attributeCode); updateFilter(newFilters); }; return ( <> {availableAttributes.map((attribute) => { const selectedCount = getSelectedCount(attribute.attributeCode); const filteredOptions = getFilteredOptions(attribute); const isCollapsed = collapsedAttributes[attribute.attributeCode]; return (
    {selectedCount > 0 && ( )}
    {!isCollapsed && (
    {attribute.options.length > 5 && (
    setSearchTerms((prev) => ({ ...prev, [attribute.attributeCode]: checked ? checked.toString() : '' })) } />
    )}
    {filteredOptions.length > 0 ? ( filteredOptions.map((option) => { const isSelected = isOptionSelected( attribute.attributeCode, option.optionId.toString() ); return (
    handleAttributeChange( attribute.attributeCode, option.optionId.toString(), checked ) } />
    ); }) ) : (
    {_('No options found for "${code}"', { code: searchTerms[attribute.attributeCode] })}
    )}
    {!searchTerms[attribute.attributeCode] && attribute.options.length > 10 && ( )}
    )}
    ); })} ); }; ================================================ FILE: packages/evershop/src/components/frontStore/catalog/DefaultCategoryFilterRender.tsx ================================================ import { Checkbox } from '@components/common/ui/Checkbox.js'; import { Label } from '@components/common/ui/Label.js'; import { CategoryFilter, FilterInput, useProductFilter } from '@components/frontStore/catalog/ProductFilter.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React, { useState } from 'react'; export const DefaultCategoryFilterRender: React.FC<{ categories: CategoryFilter[]; currentFilters: FilterInput[]; }> = ({ categories, currentFilters }) => { const { updateFilter } = useProductFilter(); const [searchTerm, setSearchTerm] = useState(''); const [isCollapsed, setIsCollapsed] = useState(false); const handleCategoryChange = (categoryId: string, checked: boolean) => { let newFilters = currentFilters.map((f) => ({ ...f })); const existingFilter = newFilters.find((f) => f.key === 'cat'); if (checked) { if (existingFilter) { const values = existingFilter.value.split(','); if (!values.includes(categoryId)) { values.push(categoryId); existingFilter.value = values.join(','); } } else { newFilters.push({ key: 'cat', operation: 'in', value: categoryId }); } } else if (existingFilter) { const values = existingFilter.value .split(',') .filter((v) => v !== categoryId); if (values.length === 0) { newFilters = newFilters.filter((f) => f.key !== 'cat'); } else { existingFilter.value = values.join(','); } } updateFilter(newFilters); }; const isCategorySelected = (categoryId: string) => { const filter = currentFilters.find((f) => f.key === 'cat'); return filter ? filter.value.split(',').includes(categoryId) : false; }; const getSelectedCount = () => { const filter = currentFilters.find((f) => f.key === 'cat'); return filter ? filter.value.split(',').length : 0; }; const clearCategoryFilter = () => { const newFilters = currentFilters.filter((f) => f.key !== 'cat'); updateFilter(newFilters); }; const getFilteredCategories = () => { if (!searchTerm) return categories; return categories.filter((category) => category.name.toLowerCase().includes(searchTerm.toLowerCase()) ); }; if (!categories || categories.length === 0) { return null; } const selectedCount = getSelectedCount(); const filteredCategories = getFilteredCategories(); return (
    {selectedCount > 0 && ( )}
    {!isCollapsed && (
    {filteredCategories.length > 0 ? ( filteredCategories.map((category) => { const isSelected = isCategorySelected( category.categoryId.toString() ); return (
    handleCategoryChange( category.categoryId.toString(), checked ) } />
    ); }) ) : (
    {_('No categories found for "${term}"', { term: searchTerm })}
    )}
    )}
    ); }; ================================================ FILE: packages/evershop/src/components/frontStore/catalog/DefaultFilterWrapperRender.tsx ================================================ import React from 'react'; export const DefaultFilterWrapperRender: React.FC<{ title: string; children: React.ReactNode; }> = ({ title, children }) => (
    {title}
    {children}
    ); ================================================ FILE: packages/evershop/src/components/frontStore/catalog/DefaultPriceFilterRender.tsx ================================================ import { Slider } from '@components/common/ui/Slider.js'; import { DefaultFilterWrapperRender } from '@components/frontStore/catalog/DefaultFilterWrapperRender.js'; import { PriceRange, FilterInput, useProductFilter, ProductFilterProps } from '@components/frontStore/catalog/ProductFilter.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React, { useState, useMemo } from 'react'; export const DefaultPriceFilterRender: React.FC<{ priceRange: PriceRange; currentFilters: FilterInput[]; setting?: ProductFilterProps['setting']; }> = ({ priceRange, currentFilters, setting }) => { const { updateFilter } = useProductFilter(); // Initialize from current filters const [localMin, setLocalMin] = useState(() => { const minFilter = currentFilters.find((f) => f.key === 'min_price'); return minFilter ? parseInt(minFilter.value) : priceRange.min; }); const [localMax, setLocalMax] = useState(() => { const maxFilter = currentFilters.find((f) => f.key === 'max_price'); return maxFilter ? parseInt(maxFilter.value) : priceRange.max; }); const debouncedUpdate = useMemo(() => { let timeoutId: NodeJS.Timeout; return (min: number, max: number) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => { const newFilters = currentFilters.filter( (f) => f.key !== 'min_price' && f.key !== 'max_price' ); if (min > priceRange.min) { newFilters.push({ key: 'min_price', operation: 'eq', value: min.toString() }); } if (max < priceRange.max) { newFilters.push({ key: 'max_price', operation: 'eq', value: max.toString() }); } updateFilter(newFilters); }, 300); // 300ms debounce }; }, [currentFilters, priceRange, updateFilter]); // Sync with external filter changes React.useEffect(() => { const minFilter = currentFilters.find((f) => f.key === 'min_price'); const maxFilter = currentFilters.find((f) => f.key === 'max_price'); setLocalMin(minFilter ? parseInt(minFilter.value) : priceRange.min); setLocalMax(maxFilter ? parseInt(maxFilter.value) : priceRange.max); }, [currentFilters, priceRange]); const handleRangeChange = (values: number[]) => { const [min, max] = values; setLocalMin(min); setLocalMax(max); debouncedUpdate(min, max); }; return (
    {priceRange.minText} {priceRange.maxText}
    ); }; ================================================ FILE: packages/evershop/src/components/frontStore/catalog/DefaultProductFilterRender.tsx ================================================ import Area from '@components/common/Area.js'; import { Button } from '@components/common/ui/Button.js'; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from '@components/common/ui/Sheet.js'; import { DefaultAttributeFilterRender } from '@components/frontStore/catalog/DefaultAttributeFilterRender.js'; import { DefaultCategoryFilterRender } from '@components/frontStore/catalog/DefaultCategoryFilterRender.js'; import { DefaultPriceFilterRender as PriceFilterRenderer } from '@components/frontStore/catalog/DefaultPriceFilterRender.js'; import { DefaultProductFilterSummary } from '@components/frontStore/catalog/DefaultProductFilterSummary.js'; import { ProductFilterRenderProps, FilterComponent, ProductFilterDispatch } from '@components/frontStore/catalog/ProductFilter.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import { useState, useMemo } from 'react'; import React from 'react'; export const DefaultProductFilterRender: React.FC<{ renderProps: ProductFilterRenderProps; className?: string; title?: string; showFilterSummary?: boolean; }> = ({ renderProps, className = '', title = _('Filter Products'), showFilterSummary = true }) => { const [isMobileFilterOpen, setIsMobileFilterOpen] = useState(false); const { currentFilters, availableAttributes, priceRange, categories, setting, removeFilter, updateFilter, clearAllFilters, isLoading, activeFilterCount } = renderProps; const defaultComponents = useMemo(() => { const components: FilterComponent[] = []; if (priceRange && priceRange.min !== priceRange.max) { components.push({ component: { default: PriceFilterRenderer }, props: { priceRange, currentFilters, setting }, sortOrder: 10, id: 'priceFilter' }); } if (categories.length > 0) { components.push({ component: { default: DefaultCategoryFilterRender }, props: { categories, currentFilters }, sortOrder: 15, id: 'categoryFilter' }); } if (availableAttributes.length > 0) { components.push({ component: { default: DefaultAttributeFilterRender }, props: { availableAttributes, currentFilters }, sortOrder: 20, id: 'attributeFilter' }); } return components; }, [availableAttributes, priceRange, categories, currentFilters, setting]); const contextValue = useMemo( () => ({ updateFilter, removeFilter, clearAllFilters }), [updateFilter, removeFilter, clearAllFilters] ); return ( {_('Filters')}
    {showFilterSummary && ( )}
    {title && (

    {title}

    )} {activeFilterCount > 0 && ( )}
    {showFilterSummary && ( )}
    ); }; ================================================ FILE: packages/evershop/src/components/frontStore/catalog/DefaultProductFilterSummary.tsx ================================================ import { Item, ItemContent, ItemDescription, ItemTitle } from '@components/common/ui/Item.js'; import { CategoryFilter, FilterableAttribute, FilterInput, PriceRange } from '@components/frontStore/catalog/ProductFilter.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; export const formatPrice = (oldFormatted: string, price: number) => { const match = oldFormatted.match(/^[^\d.,]+/); const currencySymbol = match ? match[0] : ''; return currencySymbol + price; }; export const getFilterSummary = ( availableAttributes, currentFilters, priceRange, categories ) => { const summaries: string[] = []; // Price filters const minPrice = currentFilters.find((f) => f.key === 'min_price'); const maxPrice = currentFilters.find((f) => f.key === 'max_price'); if (minPrice || maxPrice) { const min = minPrice?.value || priceRange?.min.toString() || '0'; const max = maxPrice?.value || priceRange?.max.toString() || '∞'; summaries.push( _('Price: ${value}', { value: `${formatPrice( priceRange.minText, parseInt(min) )} - ${formatPrice(priceRange.maxText, parseInt(max))}` }) ); } const categoryFilter = currentFilters.find((f) => f.key === 'cat'); if (categoryFilter) { const selectedCategoryIds = categoryFilter.value.split(','); const selectedCategories = categories.filter((cat) => selectedCategoryIds.includes(cat.categoryId.toString()) ); if (selectedCategories.length > 0) { summaries.push( `${_('Categories')}: ${selectedCategories .map((c) => c.name) .join(', ')}` ); } } availableAttributes.forEach((attr) => { const filter = currentFilters.find((f) => f.key === attr.attributeCode); if (filter) { const selectedOptionIds = filter.value.split(','); const selectedOptions = attr.options.filter((opt) => selectedOptionIds.includes(opt.optionId.toString()) ); if (selectedOptions.length > 0) { summaries.push( `${attr.attributeName}: ${selectedOptions .map((o) => o.optionText) .join(', ')}` ); } } }); return summaries; }; export const DefaultProductFilterSummary: React.FC<{ availableAttributes: FilterableAttribute[]; currentFilters: FilterInput[]; priceRange?: PriceRange; categories: CategoryFilter[]; }> = ({ availableAttributes, currentFilters, priceRange, categories }) => { const filterSummary = getFilterSummary( availableAttributes, currentFilters, priceRange, categories ); if (filterSummary.length === 0) { return null; } return ( {_('Active Filters')}
    {filterSummary.map((summary, index) => (
    {summary}
    ))}
    ); }; ================================================ FILE: packages/evershop/src/components/frontStore/catalog/DefaultVariantSelectorRender.tsx ================================================ import { Button } from '@components/common/ui/Button.js'; import { VariantAttributeGroupProps, VariantOptionItemProps } from '@components/frontStore/catalog/VariantSelector.js'; import React from 'react'; const DefaultVariantOptionItem: React.FC = ({ option, attribute, isSelected, onSelect }) => { let className = 'group '; if (isSelected) { className += 'selected'; } if (option.available === false) { className += 'un-available'; } return (
  • ); }; const DefaultVariantAttribute: React.FC = ({ attribute, options, onSelect, OptionItem = DefaultVariantOptionItem }) => { return (
    {attribute.attributeName}
      {options.map((option) => ( ))}
    ); }; export { DefaultVariantAttribute, DefaultVariantOptionItem }; ================================================ FILE: packages/evershop/src/components/frontStore/catalog/Media.scss ================================================ .product-media-container { position: relative; width: 100%; margin-bottom: 2rem; .slick-arrow { position: absolute; bottom: 20px !important; top: auto !important; z-index: 10; width: 40px; height: 40px; border-radius: 50%; background-color: rgba(255, 255, 255, 0.7); display: flex !important; align-items: center; justify-content: center; &:hover { background-color: rgba(255, 255, 255, 0.9); } &::before { color: #333; font-size: 16px; opacity: 0.8; } &.slick-disabled { opacity: 0.4; } } .slick-prev { right: 70px !important; left: auto !important; } .slick-next { right: 20px !important; } .main-image-container { position: relative; margin-bottom: 1rem; border-radius: 8px; overflow: hidden; .product-image { display: flex !important; justify-content: center; align-items: center; background-color: #f7f7f7; cursor: pointer; img { max-width: 100%; height: auto; object-fit: contain; } } } .slick-dots.slick-thumb { position: static; display: flex !important; flex-wrap: wrap; justify-content: center; margin-top: 15px; width: 100%; padding-right: 130px; li { width: auto; height: auto; margin: 0 5px 10px; border-radius: 4px; transition: all 0.2s ease; opacity: 0.7; button { padding: 0; font-size: 0; line-height: 0; } .thumbnail-wrapper { width: 70px; height: 70px; border: 1px solid #e0e0e0; border-radius: 4px; padding: 2px; display: flex; align-items: center; justify-content: center; overflow: hidden; background: #fff; transition: all 0.3s ease; } &.slick-active { opacity: 1; transform: scale(1.05); .thumbnail-wrapper { border-color: #999999; } } &:hover .thumbnail-wrapper { transform: translateY(-2px); } img { max-width: 100%; max-height: 100%; object-fit: contain; } } } .product-image-modal .slick-dots.slick-thumb { margin-top: 20px; li .thumbnail-wrapper { width: 60px; height: 60px; background: rgba(255, 255, 255, 0.9); } } .product-image-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 1000; display: flex; justify-content: center; align-items: center; .modal-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.7); } .modal-content { position: relative; width: 100%; height: 100%; z-index: 1001; background-color: black; overflow: hidden; .modal-close { position: absolute; top: 20px; right: 20px; background: rgba(255, 255, 255, 0.2); border: none; color: white; width: 40px; height: 40px; border-radius: 50%; font-size: 24px; line-height: 1; cursor: pointer; z-index: 1002; display: flex; align-items: center; justify-content: center; transition: background-color 0.2s; &:hover { background-color: rgba(0, 0, 0, 0.5); } } .modal-slider-container { width: 100%; height: 100vh; display: flex; flex-direction: column; justify-content: center; position: relative; .loading-indicator { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 1003; color: white; .spinner { animation: spin 1.5s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } } .modal-image { display: flex !important; justify-content: center; align-items: center; height: calc(100vh - 100px); img { max-width: 100%; max-height: 90vh; object-fit: contain; } } .slick-dots.slick-thumb { position: absolute; bottom: 20px; left: 0; right: 0; margin: 0; padding: 0 170px 0 20px; li { margin: 0 8px; .thumbnail-wrapper { width: 60px; height: 60px; border-color: rgba(255, 255, 255, 0.3); background-color: rgba(0, 0, 0, 0.3); border-radius: 4px; } &.slick-active .thumbnail-wrapper { border-color: white; transform: translateY(-5px); } &:hover .thumbnail-wrapper { border-color: rgba(255, 255, 255, 0.8); } } } } .slick-arrow { position: absolute; bottom: 30px !important; top: auto !important; z-index: 1002; width: 50px; height: 50px; background-color: rgba(255, 255, 255, 0.2); border-radius: 50%; transition: all 0.2s ease; display: flex !important; align-items: center; justify-content: center; &:before { font-size: 30px; opacity: 1; } &:hover { background-color: rgba(255, 255, 255, 0.4); } } .slick-prev { right: 110px !important; left: auto !important; } .slick-next { right: 40px !important; } } } .slick-slide { outline: none; } .custom-arrow { display: flex !important; align-items: center; justify-content: center; color: #333; svg { color: #fff; } &:hover { color: #000; } &:before { display: none !important; } } .slick-dots { bottom: -30px; li button:before { font-size: 8px; } } .slick-prev { left: -10px; z-index: 1; } .slick-next { right: -10px; z-index: 1; } } ================================================ FILE: packages/evershop/src/components/frontStore/catalog/Media.tsx ================================================ /* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable jsx-a11y/no-static-element-interactions */ import React, { useState, useRef } from 'react'; import Slider from 'react-slick'; import 'slick-carousel/slick/slick.css'; import 'slick-carousel/slick/slick-theme.css'; import { Image } from '@components/common/Image.js'; import { useProduct } from '@components/frontStore/catalog/ProductContext.js'; import './Media.scss'; import { ProductNoThumbnail } from '@components/common/ProductNoThumbnail.js'; const SliderComponent = Slider as any; type SliderType = any; const PrevArrow = (props: any) => { const { className, onClick } = props; return ( ); }; const NextArrow = (props: any) => { const { className, onClick } = props; return ( ); }; interface ImageWithDimensionsProps { url: string; alt?: string; width: number; height: number; } interface MediaProps { imageSize?: { width: number; height: number; }; thumbnailSize?: { width: number; height: number; }; modalSize?: { width: number; height: number; }; } export const Media: React.FC = ({ imageSize = { width: 600, height: 600 }, thumbnailSize = { width: 100, height: 100 }, modalSize = { width: 1200, height: 1200 } }) => { const product = useProduct(); const [isModalOpen, setIsModalOpen] = useState(false); const [activeSlide, setActiveSlide] = useState(0); const [isImageLoading, setIsImageLoading] = useState(false); const mainSliderRef = useRef(null); const modalSliderRef = useRef(null); const allImages: ImageWithDimensionsProps[] = []; const fullscreenWidth = modalSize.width * 1.5; const fullscreenHeight = modalSize.height * 1.5; if (product.image) { allImages.push({ url: product.image.url, alt: product.image.alt || product.name, width: imageSize.width, height: imageSize.height }); } if (product.gallery && Array.isArray(product.gallery)) { product.gallery.forEach((img) => { allImages.push({ url: img.url, alt: img.alt || product.name, width: imageSize.width, height: imageSize.height }); }); } const mainSliderSettings = { dots: allImages.length > 1, dotsClass: 'slick-dots slick-thumb', infinite: true, speed: 500, slidesToShow: 1, slidesToScroll: 1, arrows: allImages.length > 1, fade: false, prevArrow: , nextArrow: , beforeChange: (_: number, next: number) => { setActiveSlide(next); }, customPaging: function (i: number) { return (
    {`Thumbnail
    ); } }; const modalSliderSettings = { dots: allImages.length > 1, dotsClass: 'slick-dots slick-thumb', infinite: true, speed: 500, slidesToShow: 1, slidesToScroll: 1, arrows: true, prevArrow: , nextArrow: , initialSlide: activeSlide, adaptiveHeight: true, lazyLoad: 'ondemand', fade: false, swipe: true, beforeChange: () => setIsImageLoading(true), afterChange: () => setIsImageLoading(false), customPaging: function (i: number) { return (
    {`Thumbnail
    ); } }; const openModal = (index: number) => { setActiveSlide(index); setIsModalOpen(true); setTimeout(() => { if (modalSliderRef.current) { modalSliderRef.current.slickGoTo(index); } }, 100); document.addEventListener('keydown', handleKeyDown); document.body.style.overflow = 'hidden'; }; const closeModal = () => { setIsModalOpen(false); document.removeEventListener('keydown', handleKeyDown); document.body.style.overflow = ''; }; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { closeModal(); } else if (e.key === 'ArrowRight' && modalSliderRef.current) { modalSliderRef.current.slickNext(); } else if (e.key === 'ArrowLeft' && modalSliderRef.current) { modalSliderRef.current.slickPrev(); } }; return (
    {allImages.length > 0 && ( {allImages.map((image, index) => (
    openModal(index)} style={{ width: imageSize.width, height: imageSize.height }} > {image.alt
    ))}
    )} {allImages.length === 0 && (
    )}
    {isModalOpen && (
    {isImageLoading && (
    )} {allImages.map((image, index) => (
    {image.alt
    ))}
    )}
    ); }; ================================================ FILE: packages/evershop/src/components/frontStore/catalog/ProductContext.tsx ================================================ import { Row } from '@components/common/form/Editor.js'; import React, { createContext, useContext, ReactNode } from 'react'; export interface ProductPrice { value: number; text: string; } export interface ProductPriceData { regular: ProductPrice; special?: ProductPrice; } export interface AttributeOption { optionId: number; optionText: string; productId?: number; } export interface VariantAttribute { attributeId: number; attributeCode: string; attributeName: string; options: AttributeOption[]; } export interface ImageData { url: string; alt?: string; } export interface AttributeIndexItem { attributeName: string; attributeCode: string; optionId: number; optionText: string; } export interface VariantGroup { variantAttributes: VariantAttribute[]; items: { attributes: { attributeCode: string; optionId: number; }[]; product?: { productId: number; name: string; sku: string; url: string; price: ProductPriceData; image?: ImageData; }; }[]; } export interface ProductData { productId: number; uuid: string; name: string; description: Array; sku: string; price: ProductPriceData; inventory: { isInStock: boolean; }; weight?: { value: number; unit: string; }; url?: string; image?: ImageData; gallery?: ImageData[]; attributes?: AttributeIndexItem[]; variantGroup?: VariantGroup; [extendedFields: string]: any; } const ProductContext = createContext(undefined); interface ProductProviderProps { children: ReactNode; product: ProductData; } export const ProductProvider: React.FC = ({ children, product }) => { return ( {children} ); }; export const useProduct = (): ProductData => { const context = useContext(ProductContext); if (context === undefined) { throw new Error('useProduct must be used within a ProductProvider'); } return context; }; ================================================ FILE: packages/evershop/src/components/frontStore/catalog/ProductFilter.tsx ================================================ import { useAppDispatch } from '@components/common/context/app.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React, { useState, useContext, useCallback } from 'react'; export interface FilterInput { key: string; operation: 'eq' | 'in' | 'range' | 'gt' | 'lt'; value: string; } export interface FilterableAttribute { attributeCode: string; attributeName: string; attributeId: number; options: Array<{ optionId: number; optionText: string; }>; } export interface PriceRange { min: number; minText: string; max: number; maxText: string; } export interface CategoryFilter { categoryId: number; name: string; uuid: string; } export interface FilterComponent { component: { default: React.ComponentType }; props: Record; sortOrder: number; id?: string; } export interface ProductFilterRenderProps { currentFilters: FilterInput[]; availableAttributes: FilterableAttribute[]; priceRange?: PriceRange; categories: CategoryFilter[]; setting?: { storeLanguage: string; storeCurrency: string; }; updateFilter: (filters: FilterInput[]) => void; clearAllFilters: () => void; addFilter: ( key: string, operation: FilterInput['operation'], value: string ) => void; removeFilter: (key: string) => void; removeFilterValue: (key: string, value: string) => void; toggleFilter: ( key: string, operation: FilterInput['operation'], value: string ) => void; hasFilter: (key: string) => boolean; getFilterValue: (key: string) => string | undefined; isLoading: boolean; activeFilterCount: number; isOptionSelected: (attributeCode: string, optionId: string) => boolean; isCategorySelected: (categoryId: string) => boolean; getSelectedCount: (attributeCode: string) => number; getCategorySelectedCount: () => number; } export interface ProductFilterProps { currentFilters: FilterInput[]; availableAttributes?: FilterableAttribute[]; priceRange: PriceRange; categories?: CategoryFilter[]; setting?: { storeLanguage: string; storeCurrency: string; }; onFilterUpdate?: (filters: FilterInput[]) => void; children: (props: ProductFilterRenderProps) => React.ReactNode; } // Create a context for filter dispatch export const ProductFilterDispatch = React.createContext<{ updateFilter: (filters: FilterInput[]) => void; } | null>(null); // Hook to use the filter context export const useProductFilter = () => { const context = useContext(ProductFilterDispatch); if (!context) { throw new Error( 'useProductFilter must be used within a ProductFilterProvider' ); } return context; }; export const ProductFilter: React.FC = ({ currentFilters, availableAttributes = [], priceRange, categories = [], setting, onFilterUpdate, children }) => { const AppContextDispatch = useAppDispatch(); const [isLoading, setIsLoading] = useState(false); const defaultUpdateFilter = async (newFilters: FilterInput[]) => { setIsLoading(true); try { const currentUrl = window.location.href; const url = new URL(currentUrl, window.location.origin); for (const filter of currentFilters) { if (['page', 'limit', 'ob', 'od'].includes(filter.key)) { continue; } if (filter.operation === 'eq') { url.searchParams.delete(filter.key); } else { url.searchParams.delete(`${filter.key}[operation]`); url.searchParams.delete(`${filter.key}[value]`); } } // Add new filter parameters for (const filter of newFilters) { if (['page', 'limit', 'ob', 'od'].includes(filter.key)) { continue; } if (filter.operation === 'eq') { url.searchParams.append(filter.key, filter.value); } else { url.searchParams.append(`${filter.key}[operation]`, filter.operation); url.searchParams.append(`${filter.key}[value]`, filter.value); } } url.searchParams.delete('page'); url.searchParams.append('ajax', 'true'); // Update page data via GraphQL await AppContextDispatch.fetchPageData(url); url.searchParams.delete('ajax'); history.pushState(null, '', url); } catch (error) { //eslint-disable-next-line no-console console.error('Failed to update filters:', error); } finally { setIsLoading(false); } }; const updateFilter = onFilterUpdate || defaultUpdateFilter; // Filter management functions const addFilter = useCallback( (key: string, operation: FilterInput['operation'], value: string) => { let newFilters: FilterInput[]; // For 'in' operation, handle multiple values if (operation === 'in') { const existingFilter = currentFilters.find( (f) => f.key === key && f.operation === 'in' ); if (existingFilter) { // Add value to existing 'in' filter if not already present const existingValues = existingFilter.value.split(','); if (!existingValues.includes(value)) { const newValues = [...existingValues, value].join(','); newFilters = currentFilters.map((f) => f.key === key && f.operation === 'in' ? { ...f, value: newValues } : f ); } else { return; } } else { newFilters = currentFilters.filter((f) => f.key !== key); newFilters.push({ key, operation, value }); } } else { newFilters = currentFilters.filter((f) => f.key !== key); newFilters.push({ key, operation, value }); } updateFilter(newFilters); }, [currentFilters, updateFilter] ); const removeFilter = useCallback( (key: string) => { const newFilters = currentFilters.filter((f) => f.key !== key); updateFilter(newFilters); }, [currentFilters, updateFilter] ); const removeFilterValue = useCallback( (key: string, value: string) => { const filter = currentFilters.find( (f) => f.key === key && f.operation === 'in' ); if (filter) { const values = filter.value.split(','); const newValues = values.filter((v) => v !== value); if (newValues.length === 0) { const newFilters = currentFilters.filter( (f) => !(f.key === key && f.operation === 'in') ); updateFilter(newFilters); } else if (newValues.length === 1) { const newFilters = currentFilters.map((f) => f.key === key && f.operation === 'in' ? { key, operation: 'eq' as const, value: newValues[0] } : f ); updateFilter(newFilters); } else { const newFilters = currentFilters.map((f) => f.key === key && f.operation === 'in' ? { ...f, value: newValues.join(',') } : f ); updateFilter(newFilters); } } else { const newFilters = currentFilters.filter((f) => f.key !== key); updateFilter(newFilters); } }, [currentFilters, updateFilter] ); const toggleFilter = useCallback( (key: string, operation: FilterInput['operation'], value: string) => { // For 'in' operation, handle multi-value toggle if (operation === 'in') { const filter = currentFilters.find( (f) => f.key === key && f.operation === 'in' ); if (filter) { const values = filter.value.split(','); if (values.includes(value)) { removeFilterValue(key, value); } else { addFilter(key, operation, value); } } else { addFilter(key, operation, value); } } else { const hasFilter = currentFilters.some((f) => f.key === key); if (hasFilter) { removeFilter(key); } else { addFilter(key, operation, value); } } }, [currentFilters, addFilter, removeFilter, removeFilterValue] ); const clearAllFilters = useCallback(() => { setIsLoading(true); const url = new URL(window.location.href, window.location.origin); for (const key of [...url.searchParams.keys()]) { if (!['page', 'limit', 'ob', 'od'].includes(key)) { url.searchParams.delete(key); } } url.searchParams.append('ajax', 'true'); AppContextDispatch.fetchPageData(url) .then(() => { url.searchParams.delete('ajax'); history.pushState(null, '', url); }) .finally(() => setIsLoading(false)); }, [currentFilters, updateFilter]); const hasFilter = useCallback( (key: string) => { return currentFilters.some((f) => f.key === key); }, [currentFilters] ); const getFilterValue = useCallback( (key: string) => { return currentFilters.find((f) => f.key === key)?.value; }, [currentFilters] ); const isOptionSelected = useCallback( (attributeCode: string, optionId: string) => { const filter = currentFilters.find((f) => f.key === attributeCode); return filter ? filter.value.split(',').includes(optionId.toString()) : false; }, [currentFilters] ); const isCategorySelected = useCallback( (categoryId: string) => { const filter = currentFilters.find((f) => f.key === 'cat'); return filter ? filter.value.split(',').includes(categoryId) : false; }, [currentFilters] ); const getSelectedCount = useCallback( (attributeCode: string) => { const filter = currentFilters.find((f) => f.key === attributeCode); return filter ? filter.value.split(',').length : 0; }, [currentFilters] ); const getCategorySelectedCount = useCallback(() => { const filter = currentFilters.find((f) => f.key === 'cat'); return filter ? filter.value.split(',').length : 0; }, [currentFilters]); const activeFilterCount = currentFilters.filter( (f) => !['page', 'limit', 'ob', 'od'].includes(f.key) ).length; const renderProps: ProductFilterRenderProps = { currentFilters, availableAttributes, priceRange, categories, setting, updateFilter, clearAllFilters, addFilter, removeFilter, removeFilterValue, toggleFilter, hasFilter, getFilterValue, isLoading, activeFilterCount, isOptionSelected, isCategorySelected, getSelectedCount, getCategorySelectedCount }; return children(renderProps); }; ================================================ FILE: packages/evershop/src/components/frontStore/catalog/ProductList.tsx ================================================ import { ProductData } from '@components/frontStore/catalog/ProductContext.js'; import { ProductListEmptyRender } from '@components/frontStore/catalog/ProductListEmptyRender.js'; import { ProductListItemRender } from '@components/frontStore/catalog/ProductListItemRender.js'; import { ProductListLoadingSkeleton } from '@components/frontStore/catalog/ProductListLoadingSkeleton.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React, { ReactNode } from 'react'; export interface ProductListProps { products: ProductData[]; imageWidth?: number; imageHeight?: number; isLoading?: boolean; emptyMessage?: string | ReactNode; className?: string; layout?: 'grid' | 'list'; gridColumns?: number; showAddToCart?: boolean; customAddToCartRenderer?: (product: ProductData) => ReactNode; renderItem?: (product: ProductData) => ReactNode; } export const ProductList: React.FC = ({ products = [], imageWidth = 300, imageHeight = 300, isLoading = false, emptyMessage = _('No products found'), className = '', layout = 'grid', gridColumns = 4, showAddToCart = false, customAddToCartRenderer, renderItem }) => { if (isLoading) { return ( ); } if (!products || products.length === 0) { return ; } const layoutClass = layout === 'grid' ? 'product__grid' : 'product__list'; // Compute responsive grid columns class based on gridColumns const gridClassName = (() => { switch (gridColumns) { case 1: return 'grid-cols-1'; case 2: return 'grid-cols-1 md:grid-cols-2 gap-8'; case 3: return 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8'; case 4: return 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6'; case 5: return 'grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6'; case 6: return 'grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-6'; default: return 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6'; } })(); const styleClasses = layout === 'grid' ? 'grid' : 'flex flex-col'; const containerClass = `${layoutClass} ${gridClassName} ${className} ${styleClasses}`; const itemImageWidth = layout === 'list' ? (imageWidth > 150 ? 150 : imageWidth) : imageWidth; const itemImageHeight = layout === 'list' ? (imageHeight > 150 ? 150 : imageHeight) : imageHeight; return (
    {products.map((product) => (
    {renderItem ? ( renderItem(product) ) : ( )}
    ))}
    ); }; ================================================ FILE: packages/evershop/src/components/frontStore/catalog/ProductListEmptyRender.tsx ================================================ import React, { ReactNode } from 'react'; export const ProductListEmptyRender = ({ message }: { message: string | ReactNode; }) => { return (
    {typeof message === 'string' ?

    {message}

    : message}
    ); }; ================================================ FILE: packages/evershop/src/components/frontStore/catalog/ProductListItemRender.tsx ================================================ import { Image } from '@components/common/Image.js'; import { ProductNoThumbnail } from '@components/common/ProductNoThumbnail.js'; import { Button } from '@components/common/ui/Button.js'; import { AddToCart } from '@components/frontStore/cart/AddToCart.js'; import { ProductData } from '@components/frontStore/catalog/ProductContext.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React, { ReactNode } from 'react'; import { toast } from 'react-toastify'; export const ProductListItemRender = ({ product, imageWidth, imageHeight, layout = 'grid', showAddToCart = false, customAddToCartRenderer }: { product: ProductData; imageWidth?: number; imageHeight?: number; layout?: 'grid' | 'list'; showAddToCart?: boolean; customAddToCartRenderer?: (product: ProductData) => ReactNode; }) => { if (layout === 'list') { return (

    {product.name}

    {_('SKU ${sku}', { sku: product.sku })}
    {product.price.special && product.price.regular < product.price.special ? (
    {product.price.regular.text} {product.price.special.text}
    ) : ( {product.price.regular.text} )}
    {product.inventory.isInStock ? ( {_('In Stock')} ) : ( {_('Out of Stock')} )}
    {showAddToCart && (
    {customAddToCartRenderer ? ( customAddToCartRenderer(product) ) : ( toast.error(error)} > {(state, actions) => ( )} )}
    )}
    ); } return (
    {product.image && ( {product.image.alt )} {!product.image && ( )}

    {product.name}

    {product.price.special && product.price.regular < product.price.special ? ( <> {product.price.regular.text} {product.price.special.text} ) : ( {product.price.regular.text} )}
    {showAddToCart && (
    {customAddToCartRenderer ? ( customAddToCartRenderer(product) ) : ( toast.error(error)} > {(state, actions) => ( )} )}
    )}
    ); }; ================================================ FILE: packages/evershop/src/components/frontStore/catalog/ProductListLoadingSkeleton.tsx ================================================ import React from 'react'; interface LoadingSkeletonProps { count?: number; gridColumns?: number; layout?: 'grid' | 'list'; } export const ProductListLoadingSkeleton = ({ count = 4, gridColumns = 4, layout = 'grid' }: LoadingSkeletonProps) => { if (layout === 'list') { return (
    {Array.from({ length: count }).map((_, i) => (
    ))}
    ); } // Compute responsive grid columns class based on gridColumns const className = (() => { switch (gridColumns) { case 1: return 'grid-cols-1'; case 2: return 'grid-cols-1 md:grid-cols-2'; case 3: return 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3'; case 4: return 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4'; case 5: return 'grid-cols-1 md:grid-cols-2 lg:grid-cols-5'; case 6: return 'grid-cols-1 md:grid-cols-2 lg:grid-cols-6'; default: return 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4'; } })(); return (
    {Array.from({ length: count }).map((_, i) => (
    ))}
    ); }; ================================================ FILE: packages/evershop/src/components/frontStore/catalog/ProductSingleAttributes.tsx ================================================ import Area from '@components/common/Area.js'; import { useProduct } from '@components/frontStore/catalog/ProductContext.js'; import React from 'react'; export const ProductSingleAttributes = () => { const { attributes, sku } = useProduct(); const list = attributes ? [ { attributeCode: 'sku', attributeName: 'SKU', optionText: sku }, ...attributes ] : []; if (!list || list.length === 0) { return null; } return ( <>
      {list.map((attribute) => (
    • {attribute.attributeName}: {' '} {attribute.optionText}
    • ))}
    ); }; ================================================ FILE: packages/evershop/src/components/frontStore/catalog/ProductSingleDescription.tsx ================================================ import Area from '@components/common/Area.js'; import { Editor } from '@components/common/Editor.js'; import { useProduct } from '@components/frontStore/catalog/ProductContext.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; export const ProductSingleDescription = () => { const { description } = useProduct(); return ( <>

    {_('Product Description')}

    ); }; ================================================ FILE: packages/evershop/src/components/frontStore/catalog/ProductSingleForm.tsx ================================================ import Area from '@components/common/Area.js'; import { Form } from '@components/common/form/Form.js'; import { NumberField } from '@components/common/form/NumberField.js'; import { Button } from '@components/common/ui/Button.js'; import { AddToCart, AddToCartActions, AddToCartState } from '@components/frontStore/cart/AddToCart.js'; import { useProduct } from '@components/frontStore/catalog/ProductContext.js'; import { VariantSelector } from '@components/frontStore/catalog/VariantSelector.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; import { useForm } from 'react-hook-form'; import { toast } from 'react-toastify'; export function ProductSingleForm() { const { price, sku, inventory: { isInStock } } = useProduct(); const form = useForm(); const [addingToCart, setAddingToCart] = React.useState(false); return (
    {price.regular.text}
    ) }, sortOrder: 5, id: 'price' }, { component: { default: }, sortOrder: 10, id: 'variantSelector' }, { component: { default: ( { // To show the mini cart after adding a product to cart }} onError={(errorMessage) => { toast.error( errorMessage || _('Failed to add product to cart') ); }} > {(state: AddToCartState, actions: AddToCartActions) => (
    {state.isInStock === true && ( <> )} {state.isInStock === false && ( )}
    )}
    ) }, sortOrder: 30, id: 'addToCartButton' } ]} /> ); } ================================================ FILE: packages/evershop/src/components/frontStore/catalog/ProductSingleName.tsx ================================================ import Area from '@components/common/Area.js'; import { useProduct } from '@components/frontStore/catalog/ProductContext.js'; import React from 'react'; export const ProductSingleName = () => { const { name } = useProduct(); return ( <>

    {name}

    ); }; ================================================ FILE: packages/evershop/src/components/frontStore/catalog/ProductSingleSku.tsx ================================================ import Area from '@components/common/Area.js'; import { useProduct } from '@components/frontStore/catalog/ProductContext.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; export const ProductSingleSku = () => { const { sku } = useProduct(); return ( <>
    {_('SKU: ${sku}', { sku })}
    ); }; ================================================ FILE: packages/evershop/src/components/frontStore/catalog/ProductSorting.tsx ================================================ /* eslint-disable react/prop-types */ import { useAppDispatch } from '@components/common/context/app.js'; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from '@components/common/ui/Select.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import { cn } from '@evershop/evershop/lib/util/cn'; import { ArrowDownWideNarrow, ArrowUpWideNarrow } from 'lucide-react'; import React, { ReactNode, useCallback } from 'react'; export interface SortOption { code: string; name: string; label?: string; disabled?: boolean; } export interface SortState { sortBy: string; sortOrder: 'asc' | 'desc'; } interface ProductSortingProps { sortOptions?: SortOption[]; defaultSortBy?: string; defaultSortOrder?: 'asc' | 'desc'; showSortDirection?: boolean; enableUrlUpdate?: boolean; onSortChange?: (sortState: SortState) => Promise | void; renderSortSelect?: (props: { options: SortOption[]; value: string; onChange: (value: string) => void; disabled?: boolean; }) => ReactNode; renderSortDirection?: (props: { sortOrder: 'asc' | 'desc'; onToggle: () => void; disabled?: boolean; }) => ReactNode; className?: string; disabled?: boolean; count: number; } const defaultSortOptions: SortOption[] = [ { code: '', name: _('Default'), label: _('Default') }, { code: 'price', name: _('Price'), label: _('Price') }, { code: 'name', name: _('Name'), label: _('Name') } ]; export function ProductSorting({ sortOptions = defaultSortOptions, defaultSortBy = '', defaultSortOrder = 'asc', showSortDirection = true, enableUrlUpdate = true, onSortChange, renderSortSelect, renderSortDirection, className = '', disabled = false, count }: ProductSortingProps) { const AppContextDispatch = useAppDispatch(); const [sortBy, setSortBy] = React.useState(() => { // Check if this is browser or server if (typeof window !== 'undefined') { const params = new URL(document.location.href).searchParams; return params.get('ob') || defaultSortBy; } return defaultSortBy; }); const [sortOrder, setSortOrder] = React.useState<'asc' | 'desc'>(() => { // Check if this is browser or server if (typeof window !== 'undefined') { const params = new URL(document.location.href).searchParams; return (params.get('od') as 'asc' | 'desc') || defaultSortOrder; } return defaultSortOrder; }); const defaultSortChangeHandler = useCallback( async (newSortState: SortState) => { if (!enableUrlUpdate) return; const currentUrl = window.location.href; const url = new URL(currentUrl, window.location.origin); if (newSortState.sortBy === '' || newSortState.sortBy === defaultSortBy) { url.searchParams.delete('ob'); } else { url.searchParams.set('ob', newSortState.sortBy); } if (newSortState.sortOrder === defaultSortOrder) { url.searchParams.delete('od'); } else { url.searchParams.set('od', newSortState.sortOrder); } url.searchParams.append('ajax', 'true'); await AppContextDispatch.fetchPageData(url); url.searchParams.delete('ajax'); history.pushState(null, '', url); }, [AppContextDispatch, enableUrlUpdate, defaultSortBy, defaultSortOrder] ); const handleSortChange = onSortChange || defaultSortChangeHandler; const onChangeSort = useCallback( async (newSortBy: string) => { if (disabled) return; const newSortState = { sortBy: newSortBy, sortOrder }; setSortBy(newSortBy); await handleSortChange(newSortState); }, [sortOrder, handleSortChange, disabled] ); const onChangeDirection = useCallback(async () => { if (disabled) return; const newOrder: 'asc' | 'desc' = sortOrder === 'asc' ? 'desc' : 'asc'; const newSortState = { sortBy, sortOrder: newOrder }; setSortOrder(newOrder); await handleSortChange(newSortState); }, [sortBy, sortOrder, handleSortChange, disabled]); const defaultSortSelect = (props: { options: SortOption[]; value: string; onChange: (value: string) => void; disabled?: boolean; }) => ( ); const defaultSortDirection = (props: { sortOrder: 'asc' | 'desc'; onToggle: () => void; disabled?: boolean; }) => ( ); const containerContent = ( <>
    {renderSortSelect ? renderSortSelect({ options: sortOptions, value: sortBy, onChange: onChangeSort, disabled }) : defaultSortSelect({ options: sortOptions, value: sortBy, onChange: onChangeSort, disabled })}
    {showSortDirection && (
    {renderSortDirection ? renderSortDirection({ sortOrder, onToggle: onChangeDirection, disabled }) : defaultSortDirection({ sortOrder, onToggle: onChangeDirection, disabled })}
    )} ); return (
    {_('${count} Products', { count: count.toString() })}
    {containerContent}
    ); } ================================================ FILE: packages/evershop/src/components/frontStore/catalog/SearchBox.tsx ================================================ import { Image } from '@components/common/Image.js'; import { Input } from '@components/common/ui/Input.js'; import { InputGroup, InputGroupAddon, InputGroupInput } from '@components/common/ui/InputGroup.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import { Search, X } from 'lucide-react'; import React, { useRef, useState, ReactNode, useCallback } from 'react'; import { useClient } from 'urql'; const SEARCH_PRODUCTS_QUERY = ` query Query($filters: [FilterInput]) { products(filters: $filters) { items { ...Product } } } `; const PRODUCT_FRAGMENT = ` fragment Product on Product { productId name sku price { regular { value text } special { value text } } image { url alt } url inventory { isInStock } } `; export interface SearchResult { id: string; title: string; url?: string; image?: string; price?: string; type?: 'product' | 'category' | 'page'; [key: string]: unknown; } interface SearchBoxProps { searchPageUrl: string; enableAutocomplete?: boolean; autocompleteDelay?: number; minSearchLength?: number; maxResults?: number; onSearch?: (query: string) => Promise; renderSearchInput?: (props: { value: string; onChange: (value: string) => void; onKeyDown: (event: React.KeyboardEvent) => void; onFocus: () => void; onBlur: () => void; placeholder: string; ref: React.RefObject; }) => ReactNode; renderSearchResults?: (props: { results: SearchResult[]; query: string; onSelect: (result: SearchResult) => void; isLoading: boolean; }) => ReactNode; renderSearchIcon?: () => ReactNode; renderCloseIcon?: () => ReactNode; } export function SearchBox({ searchPageUrl, enableAutocomplete = false, autocompleteDelay = 300, minSearchLength = 2, maxResults = 10, onSearch, renderSearchInput, renderSearchResults, renderSearchIcon, renderCloseIcon }: SearchBoxProps) { const InputRef = useRef(null); const searchTimeoutRef = useRef(null); const client = useClient(); const [keyword, setKeyword] = useState(''); const [showing, setShowing] = useState(false); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); const [showResults, setShowResults] = useState(false); const defaultSearchFunction = useCallback( async (query: string): Promise => { try { const result = await client .query( ` ${PRODUCT_FRAGMENT} ${SEARCH_PRODUCTS_QUERY} `, { filters: [ { key: 'keyword', operation: 'eq', value: query }, { key: 'limit', operation: 'eq', value: `${maxResults}` } ] } ) .toPromise(); if (result.error) { return []; } if (!result.data?.products?.items) { return []; } return result.data.products.items.map((product: any) => ({ id: product.productId, title: product.name, url: product.url, image: product.image?.url, price: product.price?.special?.text || product.price?.regular?.text, type: 'product' as const, sku: product.sku, isInStock: product.inventory?.isInStock })); } catch (error) { return []; } }, [client] ); const searchFunction = onSearch || defaultSearchFunction; React.useEffect(() => { const url = new URL(window.location.href); const key = url.searchParams.get('keyword'); setKeyword(key || ''); }, []); React.useEffect(() => { if (showing) { InputRef.current?.focus(); } }, [showing]); const performSearch = useCallback( async (query: string) => { if (!enableAutocomplete || query.length < minSearchLength) { setSearchResults([]); setShowResults(false); return; } setIsSearching(true); try { const results = await searchFunction(query); setSearchResults(results.slice(0, maxResults)); setShowResults(true); } catch (error) { setSearchResults([]); } finally { setIsSearching(false); } }, [enableAutocomplete, searchFunction, minSearchLength, maxResults] ); const handleInputChange = useCallback( (value: string) => { setKeyword(value); if (enableAutocomplete) { if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); } searchTimeoutRef.current = setTimeout(() => { performSearch(value); }, autocompleteDelay); } }, [enableAutocomplete, autocompleteDelay, performSearch] ); const handleResultSelect = useCallback( (result: SearchResult) => { if (result.url) { window.location.href = result.url; } else { const url = new URL(searchPageUrl, window.location.origin); url.searchParams.set('keyword', result.title); window.location.href = url.toString(); } setShowing(false); setShowResults(false); }, [searchPageUrl] ); const handleKeyDown = useCallback( (event: React.KeyboardEvent) => { if (event.key === 'Enter') { setShowResults(false); const url = new URL(searchPageUrl, window.location.origin); url.searchParams.set('keyword', keyword); window.location.href = url.toString(); } else if (event.key === 'Escape') { setShowResults(false); setShowing(false); } }, [searchPageUrl, keyword] ); const handleFocus = useCallback(() => { if ( enableAutocomplete && keyword.length >= minSearchLength && searchResults.length > 0 ) { setShowResults(true); } }, [enableAutocomplete, keyword, minSearchLength, searchResults.length]); const handleBlur = useCallback(() => { setTimeout(() => { setShowResults(false); }, 150); }, []); const defaultSearchIcon = () => ( ); const defaultCloseIcon = () => ( ); return (
    { e.preventDefault(); setShowing(!showing); }} > {renderSearchIcon ? renderSearchIcon() : defaultSearchIcon()} {showing && (
    {renderSearchInput ? renderSearchInput({ value: keyword || '', onChange: handleInputChange, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: handleBlur, placeholder: _('Search'), ref: InputRef }) : defaultSearchInput({ value: keyword || '', onChange: handleInputChange, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: handleBlur, placeholder: _('Search'), ref: InputRef })} { e.preventDefault(); setShowing(false); setShowResults(false); }} > {renderCloseIcon ? renderCloseIcon() : defaultCloseIcon()} {enableAutocomplete && showResults && (renderSearchResults ? renderSearchResults({ results: searchResults, query: keyword || '', onSelect: handleResultSelect, isLoading: isSearching }) : defaultSearchResults({ results: searchResults, query: keyword || '', onSelect: handleResultSelect, isLoading: isSearching }))}
    )}
    ); } const defaultSearchInput = (props: { value: string; onChange: (value: string) => void; onKeyDown: (event: React.KeyboardEvent) => void; onFocus: () => void; onBlur: () => void; placeholder: string; ref: React.RefObject; }) => (
    props.onChange(e.target.value)} onKeyDown={props.onKeyDown} onFocus={props.onFocus} onBlur={props.onBlur} enterKeyHint="done" className="w-full focus:outline-none" />
    ); const defaultSearchResults = (props: { results: SearchResult[]; query: string; onSelect: (result: SearchResult) => void; isLoading: boolean; }) => { return (
    {props.isLoading && (
    Searching...
    )} {!props.isLoading && props.results.length === 0 && (
    No results found for “{props.query}”
    )} {!props.isLoading && props.results.map((result) => (
    { e.preventDefault(); props.onSelect(result); }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); props.onSelect(result); } }} role="button" tabIndex={0} > {result.image && ( {result.title} )}
    {result.title}
    {result.price &&
    {result.price}
    } {result.type && (
    {result.type}
    )}
    ))}
    ); }; ================================================ FILE: packages/evershop/src/components/frontStore/catalog/SearchContext.tsx ================================================ import { ProductData } from '@components/frontStore/catalog/ProductContext.js'; import { FilterInput } from '@components/frontStore/catalog/ProductFilter.js'; import React, { createContext, useContext, ReactNode } from 'react'; export interface SearchProducts { items: ProductData[]; currentFilters: FilterInput[]; total: number; } export interface SearchPageData { keyword: string; url?: string; products: SearchProducts; [extendedFields: string]: any; } const SearchContext = createContext(undefined); interface SearchProviderProps { children: ReactNode; searchData: SearchPageData; } export const SearchProvider: React.FC = ({ children, searchData }) => { return ( {children} ); }; export const useSearch = (): SearchPageData => { const context = useContext(SearchContext); if (context === undefined) { throw new Error('useSearch must be used within a SearchProvider'); } return context; }; ================================================ FILE: packages/evershop/src/components/frontStore/catalog/SearchInfo.tsx ================================================ import Area from '@components/common/Area.js'; import { useSearch } from '@components/frontStore/catalog/SearchContext.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; export function SearchInfo() { const { keyword } = useSearch(); return ( <>

    {_('Search results for "${keyword}"', { keyword })}

    ); } ================================================ FILE: packages/evershop/src/components/frontStore/catalog/SearchProducts.tsx ================================================ import { Area } from '@components/common/index.js'; import { ProductList } from '@components/frontStore/catalog/ProductList.js'; import { useSearch } from '@components/frontStore/catalog/SearchContext.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; export function SearchProducts() { const { products } = useSearch(); return ( <>
    {_('${count} products', { count: products.total.toString() })}
    ); } ================================================ FILE: packages/evershop/src/components/frontStore/catalog/SearchProductsPagination.tsx ================================================ import { Area } from '@components/common/index.js'; import { useSearch } from '@components/frontStore/catalog/SearchContext.js'; import { Pagination, DefaultPaginationRenderer } from '@components/frontStore/Pagination.js'; import React from 'react'; export function SearchProductsPagination() { const { products } = useSearch(); const page = products.currentFilters.find((filter) => filter.key === 'page'); const limit = products.currentFilters.find( (filter) => filter.key === 'limit' ); return ( <> {(paginationProps) => ( )} ); } ================================================ FILE: packages/evershop/src/components/frontStore/catalog/VariantSelector.tsx ================================================ import { useAppDispatch, useAppState } from '@components/common/context/app.js'; import { DefaultVariantAttribute, DefaultVariantOptionItem } from '@components/frontStore/catalog/DefaultVariantSelectorRender.js'; import { useProduct, VariantAttribute, VariantGroup, AttributeOption } from '@components/frontStore/catalog/ProductContext.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React, { useEffect, useMemo } from 'react'; import { useFormContext } from 'react-hook-form'; interface SelectedOption { attributeCode: string; optionId: number; } interface ProcessedAttribute extends VariantAttribute { selected: boolean; selectedOption: number | null; options: (AttributeOption & { available: boolean })[]; } const processAttributes = ( vs: VariantGroup | undefined, attributes: VariantAttribute[], currentUrl: string ): ProcessedAttribute[] => { if (!vs) return []; const selectedOptions: SelectedOption[] = []; let newAttributes: ProcessedAttribute[]; newAttributes = attributes.map((attribute) => { const url = new URL(currentUrl); const params = new URLSearchParams(url.search).entries(); const check = Array.from(params).find( ([key, value]) => key === attribute.attributeCode && attribute.options.find( (option) => option.optionId === parseInt(value, 10) ) ); if (check) { const terms = [ ...selectedOptions, { attributeCode: check[0], optionId: parseInt(check[1], 10) } ]; const variant = vs.items.find((item) => terms.every((attr) => item.attributes.find( (term) => term.attributeCode === attr.attributeCode && parseInt(term.optionId.toString(), 10) === parseInt(attr.optionId.toString(), 10) ) ) ); if (variant) { selectedOptions.push({ attributeCode: check[0], optionId: parseInt(check[1], 10) }); return { ...attribute, selected: true, selectedOption: parseInt(check[1], 10) } as ProcessedAttribute; } else { return { ...attribute, selected: false, selectedOption: null } as ProcessedAttribute; } } else { return { ...attribute, selected: false, selectedOption: null } as ProcessedAttribute; } }); newAttributes = newAttributes.map((attribute) => { const options = attribute.options.map((option) => { const terms = selectedOptions .filter( (selected) => selected.attributeCode !== attribute.attributeCode ) .concat({ attributeCode: attribute.attributeCode, optionId: option.optionId }); const variant = vs.items.find((item) => terms.every((attr) => item.attributes.find( (term) => term.attributeCode === attr.attributeCode && term.optionId === attr.optionId ) ) ); return { ...option, available: !!variant }; }); return { ...attribute, options }; }); return newAttributes; }; export interface VariantOptionItemProps { option: AttributeOption & { available: boolean }; attribute: ProcessedAttribute; isSelected: boolean; onSelect: (attributeCode: string, optionId: number) => Promise; } export interface VariantAttributeGroupProps { attribute: ProcessedAttribute; options: (AttributeOption & { available: boolean })[]; onSelect: (attributeCode: string, optionId: number) => Promise; OptionItem?: React.ComponentType; } interface VariantsProps { AttributeRenderer?: React.ComponentType; OptionRenderer?: React.ComponentType; } export function VariantSelector({ AttributeRenderer = DefaultVariantAttribute, OptionRenderer = DefaultVariantOptionItem }: VariantsProps) { const { variantGroup: vs, productId } = useProduct(); const { config: { pageMeta: { route: { url: currentProductUrl } } } } = useAppState(); const { register, formState: { errors } } = useFormContext(); const AppContextDispatch = useAppDispatch(); const initialAttributes = useMemo( () => processAttributes(vs, vs?.variantAttributes || [], currentProductUrl), [vs, currentProductUrl] ); const [attributes, setAttributes] = React.useState(initialAttributes); const attributeRef = React.useRef(initialAttributes); const validate = () => { return !attributeRef.current.find((a) => a.selected !== true); }; useEffect(() => { const handleProductChange = () => { const newAttributes = processAttributes( vs, vs?.variantAttributes || [], currentProductUrl ); setAttributes(newAttributes); attributeRef.current = newAttributes; }; handleProductChange(); }, [vs, productId]); const handleOptionClick = async ( attributeCode: string, optionId: number ): Promise => { const url = new URL(window.location.href); url.searchParams.set('ajax', 'true'); url.searchParams.set(attributeCode, optionId.toString()); await AppContextDispatch.fetchPageData(url); url.searchParams.delete('ajax'); history.pushState(null, '', url); }; if (!vs || attributes.length === 0) { return null; } return (
    {attributes.map((attribute) => { const options = attribute.options.filter( (v, j, s) => s.findIndex((o) => o.optionId === v.optionId) === j && v.productId ); return ( ); })} {errors.variant_selected && (
    {_('Please select variant options')}
    )}
    ); } ================================================ FILE: packages/evershop/src/components/frontStore/checkout/CheckoutButton.tsx ================================================ import Area from '@components/common/Area.js'; import { Button } from '@components/common/ui/Button.js'; import { useCartState } from '@components/frontStore/cart/CartContext.js'; import { useCheckout } from '@components/frontStore/checkout/CheckoutContext.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; import { useWatch } from 'react-hook-form'; export function CheckoutButton() { const { data: { noShippingRequired, billingAddress } } = useCartState(); const { form, registeredPaymentComponents } = useCheckout(); // Watch the selected payment method const selectedPaymentMethod = useWatch({ control: form.control, name: 'paymentMethod' }); // Get the payment component for the selected method const getPaymentComponent = (methodCode: string) => { return registeredPaymentComponents[methodCode] || null; }; // Helper function to render a component safely const renderComponent = ( component: React.ComponentType | undefined, props: any ) => { return component ? React.createElement(component, props) : null; }; // Get the selected payment method component const selectedComponent = selectedPaymentMethod ? getPaymentComponent(selectedPaymentMethod) : null; if (noShippingRequired && !billingAddress) { return ( <>
    ); } return ( <>
    {selectedPaymentMethod && selectedComponent?.checkoutButtonRenderer ? ( // Render the custom checkout button for the selected payment method renderComponent(selectedComponent.checkoutButtonRenderer, { isSelected: true }) ) : ( // Default checkout button when no payment method is selected or no custom button )}
    ); } ================================================ FILE: packages/evershop/src/components/frontStore/checkout/CheckoutContext.tsx ================================================ import { useCartState, useCartDispatch } from '@components/frontStore/cart/CartContext.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import { CreateOrderResult } from '@evershop/evershop/checkout/services'; import { CheckoutData } from '@evershop/evershop/types/checkoutData'; import { produce } from 'immer'; import React, { createContext, useReducer, useContext, ReactNode, useCallback, useMemo } from 'react'; import { UseFormReturn, FieldValues } from 'react-hook-form'; interface PaymentMethod { code: string; name: string; [key: string]: unknown; } interface PaymentMethodRendererProps { isSelected: boolean; } interface PaymentMethodComponent { nameRenderer: React.ComponentType; formRenderer: React.ComponentType; checkoutButtonRenderer: React.ComponentType; } interface ShippingMethod { code: string; name: string; cost?: { value: number; text: string; }; [key: string]: unknown; } interface ShippingAddressParams { country: string; province?: string; postcode?: string; } interface CheckoutState { orderPlaced: boolean; orderId?: string; loadingStates: { placingOrder: boolean; }; allowGuestCheckout: boolean; checkoutData: CheckoutData; // Add checkout data to state registeredPaymentComponents: Record; } type CheckoutAction = | { type: 'SET_PLACING_ORDER'; payload: boolean } | { type: 'SET_ORDER_PLACED'; payload: { orderId: string } } | { type: 'SET_CHECKOUT_DATA'; payload: CheckoutData } | { type: 'UPDATE_CHECKOUT_DATA'; payload: Partial } | { type: 'CLEAR_CHECKOUT_DATA' } // Payment method component registry actions | { type: 'REGISTER_PAYMENT_COMPONENT'; payload: { code: string; component: PaymentMethodComponent }; }; const initialState: CheckoutState = { orderPlaced: false, orderId: undefined, loadingStates: { placingOrder: false }, allowGuestCheckout: false, // Default to false, will be set by provider checkoutData: {}, // Initialize empty checkout data registeredPaymentComponents: {} // Initialize empty payment component registry }; // Reducer with Immer for immutable updates const checkoutReducer = ( state: CheckoutState, action: CheckoutAction ): CheckoutState => { return produce(state, (draft) => { switch (action.type) { case 'SET_PLACING_ORDER': draft.loadingStates.placingOrder = action.payload; break; case 'SET_ORDER_PLACED': draft.orderPlaced = true; draft.orderId = action.payload.orderId; draft.loadingStates.placingOrder = false; break; case 'SET_CHECKOUT_DATA': draft.checkoutData = action.payload; break; case 'UPDATE_CHECKOUT_DATA': draft.checkoutData = { ...draft.checkoutData, ...action.payload }; break; case 'CLEAR_CHECKOUT_DATA': draft.checkoutData = {}; break; case 'REGISTER_PAYMENT_COMPONENT': draft.registeredPaymentComponents[action.payload.code] = action.payload.component; break; } }); }; // Context types interface CheckoutContextValue extends CheckoutState { cartId: string | undefined; checkoutSuccessUrl: string; loading: boolean; // Computed from loadingStates requiresShipment: boolean; // Computed from cart items form: UseFormReturn; // React Hook Form instance } interface CheckoutDispatchContextValue< T extends CreateOrderResult = CreateOrderResult > { placeOrder: () => Promise; checkout: () => Promise; getPaymentMethods: () => PaymentMethod[]; getShippingMethods: ( params?: ShippingAddressParams ) => Promise; // Checkout data management setCheckoutData: (data: CheckoutData) => void; updateCheckoutData: (data: Partial) => void; clearCheckoutData: () => void; // Payment method component registry registerPaymentComponent: ( code: string, component: PaymentMethodComponent ) => void; enableForm: () => void; disableForm: () => void; } // Contexts const CheckoutContext = createContext( undefined ); const CheckoutDispatchContext = createContext< CheckoutDispatchContextValue | undefined >(undefined); // Provider props interface CheckoutProviderProps { children: ReactNode; placeOrderApi: string; checkoutSuccessUrl: string; allowGuestCheckout?: boolean; // Optional, defaults to false form: UseFormReturn; // React Hook Form instance passed from outside enableForm: () => void; disableForm: () => void; } // Retry utility (similar to cart context) const retry = async ( fn: () => Promise, retries = 3, delay = 1000 ): Promise => { try { return await fn(); } catch (error) { if (retries > 0) { await new Promise((resolve) => setTimeout(resolve, delay)); return retry(fn, retries - 1, delay * 2); } throw error; } }; export function CheckoutProvider({ children, placeOrderApi, checkoutSuccessUrl, allowGuestCheckout = false, form, enableForm, disableForm }: CheckoutProviderProps) { const [state, dispatch] = useReducer(checkoutReducer, { ...initialState, allowGuestCheckout }); // Get cart state for computing requiresShipment and cartId const cartState = useCartState(); const cartDispatch = useCartDispatch(); const cartId = cartState?.data?.uuid; // Get payment methods - return the list from cart context const getPaymentMethods = useCallback((): PaymentMethod[] => { return (cartState.data?.availablePaymentMethods || []).map((method) => ({ code: method.code, name: method.name })); }, [cartState.data?.availablePaymentMethods]); // Get shipping methods - if params provided, fetch dynamically; otherwise return from cart context const getShippingMethods = useCallback( async (params?: ShippingAddressParams): Promise => { if (params) { // Fetch shipping methods dynamically based on address try { await cartDispatch.fetchAvailableShippingMethods(params); // Get updated methods from cart state const methods = cartState.data?.availableShippingMethods || []; return methods.map((method) => ({ code: method.code, name: method.name, cost: method.cost || { value: 0, text: 'Free' } })); } catch (error) { // Return empty array on error, let the error be handled by cart context return []; } } else { // Return the initial shipping methods from cart context return (cartState.data?.availableShippingMethods || []).map( (method) => ({ code: method.code, name: method.name, cost: method.cost || { value: 0, text: 'Free' } }) ); } }, [cartDispatch, cartState.data?.availableShippingMethods] ); // Compute requiresShipment based on cart items const requiresShipment = useMemo(() => { // Just return true for now as all products require shipping. We will get back to this when virtual/downloadable products are supported. return true; }, [cartState?.data?.items]); // Place order with loading state and error handling (original API - expects data already in cart) const placeOrder = useCallback(async () => { if (!cartId) { throw new Error('Cart ID is required to place order'); } dispatch({ type: 'SET_PLACING_ORDER', payload: true }); const response = await retry(() => fetch(placeOrderApi, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cart_id: cartId }) }) ); const json = await response.json(); if (!response.ok) { throw new Error(json.error?.message || _('Failed to place order')); } dispatch({ type: 'SET_ORDER_PLACED', payload: { orderId: json.data.uuid } }); return json.data; }, [placeOrderApi, cartId]); // New checkout method with all data submission (cart.checkoutApi) const checkout = useCallback(async () => { if (!cartId) { throw new Error(_('Cart ID is required to checkout')); } // Trigger form validation const isValid = await form.trigger(undefined, { shouldFocus: true }); if (!isValid) { return; } disableForm(); dispatch({ type: 'SET_PLACING_ORDER', payload: true }); const response = await retry(() => fetch(cartState.data?.checkoutApi, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cart_id: cartId, ...state.checkoutData }) }) ); const json = await response.json(); if (!response.ok) { enableForm(); dispatch({ type: 'SET_PLACING_ORDER', payload: false }); throw new Error(json.error?.message || _('Failed to checkout')); } dispatch({ type: 'SET_ORDER_PLACED', payload: { orderId: json.data.uuid } }); return json.data; }, [ cartState.data?.checkoutApi, cartId, state.checkoutData, form, enableForm, disableForm ]); // Checkout data management const setCheckoutData = useCallback((data: CheckoutData) => { dispatch({ type: 'SET_CHECKOUT_DATA', payload: data }); }, []); const updateCheckoutData = useCallback((data: Partial) => { dispatch({ type: 'UPDATE_CHECKOUT_DATA', payload: data }); }, []); const clearCheckoutData = useCallback(() => { dispatch({ type: 'CLEAR_CHECKOUT_DATA' }); }, []); // Payment method component registry const registerPaymentComponent = useCallback( (code: string, component: PaymentMethodComponent) => { dispatch({ type: 'REGISTER_PAYMENT_COMPONENT', payload: { code, component } }); }, [] ); const contextValue = useMemo( (): CheckoutContextValue => ({ ...state, cartId, checkoutSuccessUrl, requiresShipment, form, loading: state.loadingStates.placingOrder }), [state, cartId, checkoutSuccessUrl, requiresShipment, form] ); const dispatchMethods = useMemo( (): CheckoutDispatchContextValue => ({ placeOrder, checkout, getPaymentMethods, getShippingMethods, setCheckoutData, updateCheckoutData, clearCheckoutData, registerPaymentComponent, enableForm, disableForm }), [ placeOrder, checkout, getPaymentMethods, getShippingMethods, setCheckoutData, updateCheckoutData, clearCheckoutData, registerPaymentComponent, enableForm, disableForm ] ); return ( {children} ); } export const useCheckout = (): CheckoutContextValue => { const context = useContext(CheckoutContext); if (context === undefined) { throw new Error('useCheckout must be used within a CheckoutProvider'); } return context; }; export const useCheckoutDispatch = < T extends CreateOrderResult = CreateOrderResult >(): CheckoutDispatchContextValue => { const context = useContext(CheckoutDispatchContext); if (context === undefined) { throw new Error( 'useCheckoutDispatch must be used within a CheckoutProvider' ); } return context as CheckoutDispatchContextValue; }; export type { PaymentMethod, ShippingMethod, ShippingAddressParams, CheckoutState }; ================================================ FILE: packages/evershop/src/components/frontStore/checkout/ContactInformation.tsx ================================================ import Area from '@components/common/Area.js'; import { EmailField } from '@components/common/form/EmailField.js'; import { PasswordField } from '@components/common/form/PasswordField.js'; import { Button } from '@components/common/ui/Button.js'; import { Card, CardContent, CardHeader, CardTitle } from '@components/common/ui/Card.js'; import { Item, ItemActions, ItemContent, ItemDescription, ItemTitle } from '@components/common/ui/Item.js'; import { useCartState } from '@components/frontStore/cart/CartContext.js'; import { useCheckout, useCheckoutDispatch } from '@components/frontStore/checkout/CheckoutContext.js'; import { useCustomer, useCustomerDispatch } from '@components/frontStore/customer/CustomerContext.jsx'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import { CircleUser } from 'lucide-react'; import React, { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; const LoggedIn: React.FC<{ fullName: string; email: string; uuid: string; }> = ({ uuid, fullName, email }) => { const [isLoggingOut, setIsLoggingOut] = useState(false); const { logout } = useCustomerDispatch(); const { updateCheckoutData } = useCheckoutDispatch(); useEffect(() => { updateCheckoutData({ customer: { id: uuid, email: email, fullName: fullName } }); }, [fullName, email]); const handleLogout = async () => { if (isLoggingOut) return; try { setIsLoggingOut(true); await logout(); toast.success(_('Successfully logged out')); } catch (error) { const errorMessage = error instanceof Error ? error.message : _('Logout failed'); toast.error(errorMessage); } finally { setIsLoggingOut(false); } }; return (
    {_('Logged in as')} {fullName}
    {email}
    ); }; const Guest: React.FC<{ email: string; }> = ({ email }) => { const [showLogin, setShowLogin] = useState(false); const [isLogging, setIsLogging] = useState(false); const { login } = useCustomerDispatch(); const { form } = useCheckout(); const { updateCheckoutData } = useCheckoutDispatch(); const contactEmail = form.watch('contact.email', email); const handleLoginClick = (e: React.MouseEvent) => { e.preventDefault(); setShowLogin(true); }; useEffect(() => { updateCheckoutData({ customer: { email: contactEmail } }); }, [contactEmail]); const handleLogin = async () => { if (isLogging) return; try { setIsLogging(true); const isValid = await form.trigger(['contact.email', 'contact.password']); if (!isValid) { return; } const formData = form.getValues(); const loginEmail = formData?.contact?.email; const password = formData?.contact?.password; await login( { email: loginEmail, password: password }, window.location.href ); toast.success(_('Successfully logged in')); setShowLogin(false); } catch (error) { const errorMessage = error instanceof Error ? error.message : _('Login failed'); toast.error(errorMessage); } finally { setIsLogging(false); } }; const handleCancelLogin = () => { setShowLogin(false); // Clear password field form.setValue('contact.password', ''); }; return (
    {showLogin && (
    )} {!showLogin && (

    {_('Already have an account?')}{' '}

    )}
    ); }; export function ContactInformation() { const { customer } = useCustomer(); const { data: cart } = useCartState(); return ( <>
    {_('Contact Information')}
    {customer ? ( ) : ( )}
    ); } ================================================ FILE: packages/evershop/src/components/frontStore/checkout/OrderSummaryItems.tsx ================================================ import { useAppState } from '@components/common/context/app.js'; import { Image } from '@components/common/Image.js'; import { ProductNoThumbnail } from '@components/common/ProductNoThumbnail.js'; import { OrderItem } from '@components/frontStore/customer/CustomerContext.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; const OrderSummaryItems: React.FC<{ items: OrderItem[]; }> = ({ items }) => { const { config: { tax: { priceIncludingTax } } } = useAppState(); if (items.length === 0) { return null; } return (
      {items.map((item) => (
    • {item.thumbnail && ( {item.productName} )} {!item.thumbnail && ( )} {item.qty}
      {item.productName.length > 50 ? `${item.productName.substring(0, 50)}...` : item.productName}
      {item.variantOptions && item.variantOptions.length > 0 && (
      {item.variantOptions.map((option) => (
      {option.attributeName}: {option.optionText}
      ))}
      )}
      {priceIncludingTax ? item.lineTotalInclTax.text : item.lineTotal.text}
    • ))}
    ); }; export { OrderSummaryItems }; ================================================ FILE: packages/evershop/src/components/frontStore/checkout/OrderTotalSummary.tsx ================================================ import Area from '@components/common/Area.js'; import { useAppState } from '@components/common/context/app.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; const Total: React.FC<{ total: string; totalTaxAmount: string; priceIncludingTax: boolean; }> = ({ total, totalTaxAmount, priceIncludingTax }) => { return (
    {(priceIncludingTax && (
    {_('Total')}
    ({_('Inclusive of tax ${totalTaxAmount}', { totalTaxAmount })})
    )) || {_('Total')}}
    {total}
    ); }; const Tax: React.FC<{ showPriceIncludingTax: boolean; amount: string; }> = ({ showPriceIncludingTax, amount }) => { if (showPriceIncludingTax) { return null; } return (
    {_('Tax')}
    {amount}
    ); }; const Subtotal: React.FC<{ subTotal: string }> = ({ subTotal }) => { return (
    {_('Sub total')}
    {subTotal}
    ); }; const Discount: React.FC<{ discountAmount: string; coupon: string | undefined; }> = ({ discountAmount, coupon }) => { if (!coupon) { return null; } return (
    {_('Discount(${coupon})', { coupon })}
    - {discountAmount}
    ); }; const Shipping: React.FC<{ method: string | undefined; cost: string | undefined; }> = ({ method, cost }) => { return (
    {method && ( <> {_('Shipping (${method})', { method })}
    {cost}
    )} {!method && ( <> {_('Shipping')} {_('No shipping is required for this order')} )}
    ); }; const OrderTotalSummary: React.FC<{ subTotal: string; discountAmount: string; coupon: string | undefined; shippingMethod: string | undefined; shippingCost: string | undefined; taxAmount: string; total: string; }> = ({ subTotal, discountAmount, coupon, shippingMethod, shippingCost, taxAmount, total }) => { const { config: { tax: { priceIncludingTax } } } = useAppState(); return (
    ); }; export { OrderTotalSummary, Subtotal, Discount, Shipping, Tax, Total }; ================================================ FILE: packages/evershop/src/components/frontStore/checkout/Payment.tsx ================================================ import Area from '@components/common/Area.js'; import { Card, CardContent, CardHeader, CardTitle } from '@components/common/ui/Card.js'; import { useCartDispatch, useCartState } from '@components/frontStore/cart/CartContext.js'; import { useCheckout, useCheckoutDispatch } from '@components/frontStore/checkout/CheckoutContext.js'; import { BillingAddress } from '@components/frontStore/checkout/payment/BillingAddress.js'; import { PaymentMethods } from '@components/frontStore/checkout/payment/PaymentMethods.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import { CreditCard } from 'lucide-react'; import React, { useEffect } from 'react'; import { useWatch } from 'react-hook-form'; import { toast } from 'react-toastify'; export function Payment() { const { data: { noShippingRequired, billingAddress, availablePaymentMethods }, loadingStates: { addingBillingAddress } } = useCartState(); const { addBillingAddress } = useCartDispatch(); const { updateCheckoutData } = useCheckoutDispatch(); const { form } = useCheckout(); const paymentMethod = useWatch({ name: 'paymentMethod', control: form.control }); useEffect(() => { const updatePaymentMethod = async () => { try { const paymentMethod = form.getValues('paymentMethod'); const methodDetails = availablePaymentMethods?.find( (method) => method.code === paymentMethod ); if (!methodDetails) { throw new Error('Please select a valid payment method'); } updateCheckoutData({ paymentMethod: methodDetails.code }); } catch (error) { toast.error( error instanceof Error ? error.message : _('Failed to update shipment') ); } }; if (paymentMethod) { updatePaymentMethod(); } }, [paymentMethod]); return ( <>
    {_('Payment Information')}
    {(billingAddress || noShippingRequired === false) && ( <> ({ ...method }))} isLoading={addingBillingAddress} /> )}
    ); } ================================================ FILE: packages/evershop/src/components/frontStore/checkout/Shipment.tsx ================================================ import Area from '@components/common/Area.js'; import { Card, CardContent, CardHeader, CardTitle } from '@components/common/ui/Card.js'; import { useCartDispatch, useCartState } from '@components/frontStore/cart/CartContext.js'; import { useCheckout, useCheckoutDispatch } from '@components/frontStore/checkout/CheckoutContext.js'; import { ShippingMethods } from '@components/frontStore/checkout/shipment/ShippingMethods.js'; import CustomerAddressForm from '@components/frontStore/customer/address/addressForm/Index.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import { MapPin } from 'lucide-react'; import React, { useEffect, useRef } from 'react'; import { useWatch } from 'react-hook-form'; import { toast } from 'react-toastify'; export function Shipment() { const { data: { shippingAddress, noShippingRequired, availableShippingMethods, shippingMethod: selectedShippingMethod }, loadingStates: { fetchingShippingMethods } } = useCartState(); // Early return if no shipping is required if (noShippingRequired) { return null; } const { addShippingAddress, addShippingMethod, fetchAvailableShippingMethods } = useCartDispatch(); const { form } = useCheckout(); const { updateCheckoutData } = useCheckoutDispatch(); // Use useWatch for better performance and cleaner code const watchedShippingAddress = useWatch({ control: form.control, name: 'shippingAddress' }); const dirtyFields = form.formState.dirtyFields; const debounceTimeoutRef = useRef(null); const lastFetchParamsRef = useRef<{ country?: string; province?: string; postcode?: string; } | null>( // Initialize with current shipping address if available shippingAddress ? { country: shippingAddress.country?.code, province: shippingAddress.province?.code, postcode: shippingAddress.postcode || undefined } : null ); useEffect(() => { const fetchShippingMethods = async () => { try { const country = form.getValues('shippingAddress.country'); const province = form.getValues('shippingAddress.province'); const postcode = form.getValues('shippingAddress.postcode'); if (!country) { return; } // Check if parameters have actually changed const currentParams = { country, province, postcode }; const lastParams = lastFetchParamsRef.current; if ( lastParams && lastParams.country === country && lastParams.province === province && lastParams.postcode === postcode ) { // Parameters haven't changed, skip API call return; } // Cache the current parameters lastFetchParamsRef.current = currentParams; await fetchAvailableShippingMethods({ country, province, postcode }); } catch (error) { toast.error( error instanceof Error ? error.message : _('Failed to update shipment') ); } }; if (watchedShippingAddress && dirtyFields.shippingAddress) { // Clear existing timeout if (debounceTimeoutRef.current) { clearTimeout(debounceTimeoutRef.current); } // Set new timeout debounceTimeoutRef.current = setTimeout(() => { fetchShippingMethods(); }, 800); } // Cleanup function return () => { if (debounceTimeoutRef.current) { clearTimeout(debounceTimeoutRef.current); } }; }, [watchedShippingAddress, dirtyFields.shippingAddress]); // Clean dependency array const updateShipment = async (method: { code: string; name: string }) => { try { const validate = await form.trigger('shippingAddress'); if (!validate) { return false; } const shippingAddress = form.getValues('shippingAddress'); await addShippingAddress(shippingAddress); await addShippingMethod(method.code, method.name); updateCheckoutData({ shippingAddress, shippingMethod: method.code }); return true; } catch (error) { toast.error( error instanceof Error ? error.message : _('Failed to update shipment') ); return false; } }; return ( <>
    {_('Shipping Address')}
    ({ ...method, isSelected: method.code === selectedShippingMethod }))} shippingAddress={shippingAddress} onSelect={updateShipment} isLoading={fetchingShippingMethods} />
    ); } ================================================ FILE: packages/evershop/src/components/frontStore/checkout/payment/BillingAddress.tsx ================================================ import { Button } from '@components/common/ui/Button.js'; import { Item, ItemContent, ItemDescription, ItemTitle } from '@components/common/ui/Item.js'; import { Label } from '@components/common/ui/Label.js'; import { RadioGroup, RadioGroupItem } from '@components/common/ui/RadioGroup.js'; import { useCheckout, useCheckoutDispatch } from '@components/frontStore/checkout/CheckoutContext.js'; import CustomerAddressForm from '@components/frontStore/customer/address/addressForm/Index.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import { Address, CustomerAddressGraphql } from '@evershop/evershop/types/customerAddress'; import React, { useEffect, useState } from 'react'; import { useWatch } from 'react-hook-form'; export function BillingAddress({ billingAddress, addBillingAddress, addingBillingAddress, noShippingRequired }: { billingAddress?: CustomerAddressGraphql; addBillingAddress?: (address: Address) => Promise; addingBillingAddress?: boolean; noShippingRequired: boolean; }) { const { form, checkoutData } = useCheckout(); const { updateCheckoutData } = useCheckoutDispatch(); const { setValue, getValues, trigger, formState: { disabled } } = form; const shippingAddress = useWatch({ control: form.control, name: 'shippingAddress' }); const billingAddressField = useWatch({ control: form.control, name: 'billingAddress' }); const [useSameAddress, setUseSameAddress] = useState(!noShippingRequired); useEffect(() => { if (useSameAddress && shippingAddress) { updateCheckoutData({ billingAddress: shippingAddress }); } else if (!useSameAddress) { setValue('billingAddress', billingAddress); } }, [useSameAddress, checkoutData.shippingAddress]); useEffect(() => { if (!useSameAddress) { const billingAddress = { ...getValues('billingAddress') }; updateCheckoutData({ billingAddress }); } }, [billingAddressField]); const handleAddressOptionChange = (value: string) => { const isSameAddress = value === 'same'; if (isSameAddress === useSameAddress || disabled) { return; } setUseSameAddress(isSameAddress); if (!isSameAddress) { updateCheckoutData({ billingAddress: undefined }); } else if (checkoutData.shippingAddress) { updateCheckoutData({ billingAddress: checkoutData.shippingAddress }); } }; const handleGoToPayment = async () => { const isValid = await trigger('billingAddress'); if (isValid && addBillingAddress) { const billingAddressData = getValues('billingAddress'); await addBillingAddress(billingAddressData); } }; return (
    {_('Billing Address')} { handleAddressOptionChange(value as string); }} > {!noShippingRequired ? ( <>
    {!useSameAddress && (
    {noShippingRequired && ( )}
    )}
    ) : (
    {noShippingRequired && ( )}
    )}
    ); } ================================================ FILE: packages/evershop/src/components/frontStore/checkout/payment/PaymentMethods.tsx ================================================ import { Item, ItemContent, ItemDescription, ItemTitle } from '@components/common/ui/Item.js'; import { Label } from '@components/common/ui/Label.js'; import { RadioGroup, RadioGroupItem } from '@components/common/ui/RadioGroup.js'; import { Skeleton } from '@components/common/ui/Skeleton.js'; import { useCheckout } from '@components/frontStore/checkout/CheckoutContext.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; interface PaymentMethod { code: string; name: string; cost?: { value: number; text: string; }; description?: string; } // Skeleton component for loading state function PaymentMethodSkeleton() { return (
    {[1, 2, 3, 4].map((index) => (
    ))}
    ); } export function PaymentMethods({ methods, isLoading }: { methods: PaymentMethod[]; isLoading?: boolean; }) { const { form, registeredPaymentComponents } = useCheckout(); const { formState, watch, setValue } = form; const selectedPaymentMethod = watch('paymentMethod'); const getPaymentComponent = (methodCode: string) => { return registeredPaymentComponents[methodCode] || null; }; const renderComponent = ( component: React.ComponentType | undefined, props: any ) => { return component ? React.createElement(component, props) : null; }; return (
    {_('Pick a payment method')} {isLoading ? ( ) : ( <>
    {methods?.length === 0 ? (
    {_('No payment methods available')}
    ) : ( { setValue('paymentMethod', value); }} > {methods.map((method: PaymentMethod) => { const isSelected = selectedPaymentMethod === method.code; const component = getPaymentComponent(method.code); return (
    {component?.formRenderer && isSelected && ( {renderComponent(component.formRenderer, { isSelected })} )}
    ); })}
    )}
    {formState.errors.paymentMethod && (
    {formState.errors.paymentMethod?.message?.toString() || _('Please select a payment method')}
    )} )}
    ); } ================================================ FILE: packages/evershop/src/components/frontStore/checkout/shipment/ShippingMethods.tsx ================================================ import { Card, CardContent, CardHeader, CardTitle } from '@components/common/ui/Card.js'; import { Item, ItemActions, ItemContent, ItemDescription, ItemTitle } from '@components/common/ui/Item.js'; import { Label } from '@components/common/ui/Label.js'; import { RadioGroup, RadioGroupItem } from '@components/common/ui/RadioGroup.js'; import { useCheckout } from '@components/frontStore/checkout/CheckoutContext.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import { CustomerAddressGraphql } from '@evershop/evershop/types/customerAddress'; import { Package } from 'lucide-react'; import React from 'react'; interface ShippingMethod { code: string; name: string; cost?: { value: number; text: string; }; description?: string; isSelected?: boolean; } // Skeleton component for loading state function ShippingMethodSkeleton() { return (
    {[1, 2, 3, 4].map((index) => (
    ))}
    ); } export function ShippingMethods({ methods, shippingAddress, isLoading, onSelect }: { methods: ShippingMethod[]; shippingAddress?: CustomerAddressGraphql; isLoading?: boolean; onSelect?: (method: ShippingMethod) => Promise | boolean; }) { const { form } = useCheckout(); const { formState, setValue, watch } = form; const [isProcessing, setIsProcessing] = React.useState(false); const currentValue = watch('shippingMethod'); const handleMethodSelect = async (method: ShippingMethod) => { if (!onSelect) { // If no onSelect function provided, allow normal behavior setValue('shippingMethod', method.code); return; } if (isProcessing || formState.disabled) { return; } try { setIsProcessing(true); const result = await Promise.resolve(onSelect(method)); if (result) { // Only update the form value if onSelect returns true setValue('shippingMethod', method.code); } // If result is false, keep the current selection } catch (error) { // Keep the current selection on error } finally { setIsProcessing(false); } }; return (
    {_('Shipping Method')}
    {isLoading ? ( ) : ( <>
    {methods?.length === 0 ? (
    {!shippingAddress?.country || !shippingAddress?.province ? (
    {_( 'Available shipping methods will appear once you provide your address details' )}
    ) : (
    {_('No shipping methods available')}
    {_( 'No shipping options are available for your location' )}
    )}
    ) : ( { const method = methods.find((m) => m.code === value); if (method) { handleMethodSelect(method); } else { setValue('shippingMethod', value); } }} > {methods.map((method: ShippingMethod) => (
    { !isProcessing && handleMethodSelect(method); }} disabled={isProcessing} />
    {method.description && ( {method.description} )}
    {method.cost ? ( <> {method.cost.value > 0 ? (
    {method.cost.text}
    ) : ( <>
    {method.cost.text}
    {_('FREE')}
    )} ) : (
    {_('Contact for pricing')}
    )}
    ))}
    )}
    {formState.errors.shippingMethod && (
    {formState.errors.shippingMethod?.message?.toString() || _('Please select a shipping method')}
    )} )}
    ); } ================================================ FILE: packages/evershop/src/components/frontStore/customer/AccountInfo.tsx ================================================ import Area from '@components/common/Area.js'; import { useCustomer, useCustomerDispatch } from '@components/frontStore/customer/CustomerContext.jsx'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import { Mail, User } from 'lucide-react'; import React from 'react'; import { toast } from 'react-toastify'; interface AccountInfoProps { title?: string; showLogout?: boolean; } export default function AccountInfo({ title, showLogout }: AccountInfoProps) { const { customer: account } = useCustomer(); const { logout } = useCustomerDispatch(); return (
    {account?.fullName}
    ) }, sortOrder: 10 }, { component: { default: () => (
    {account?.email}
    ) }, sortOrder: 15 } ]} />
    ); } ================================================ FILE: packages/evershop/src/components/frontStore/customer/CustomerContext.tsx ================================================ import { useAppDispatch } from '@components/common/context/app.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import { CustomerAddressGraphql } from '@evershop/evershop/types/customerAddress'; import { produce } from 'immer'; import React, { createContext, useReducer, useContext, ReactNode, useCallback, useMemo, useEffect } from 'react'; type ExtendedCustomerAddress = CustomerAddressGraphql & { addressId: string | number; isDefault?: boolean; updateApi?: string; deleteApi?: string; }; export interface OrderItem { orderItemId: string; uuid: string; productId: string; qty: number; productSku: string; productName: string; productUrl: string; thumbnail?: string; productWeight: { value: number; unit: string; }; variantOptions?: { attributeCode: string; attributeName: string; attributeId: number; optionId: number; optionText: string; }[]; productPrice: { value: number; text: string; }; productPriceInclTax: { value: number; text: string; }; finalPrice: { value: number; text: string; }; finalPriceInclTax: { value: number; text: string; }; taxPercent: number; taxAmount: { value: number; text: string; }; taxAmountBeforeDiscount: { value: number; text: string; }; discountAmount: { value: number; text: string; }; lineTotal: { value: number; text: string; }; subTotal: { value: number; text: string; }; lineTotalWithDiscount: { value: number; text: string; }; lineTotalWithDiscountInclTax: { value: number; text: string; }; lineTotalInclTax: { value: number; text: string; }; total: { value: number; text: string; }; variantGroupId?: number; } export interface Order { orderId: number; uuid: string; orderNumber: string; currency: string; customerId?: number; customerGroupId?: number; customerEmail?: string; customerFullName?: string; coupon?: string; shippingMethod?: string; shippingMethodName?: string; paymentMethod?: string; paymentMethodName?: string; shippingNote?: string; status: { name: string; code: string; badge: string; }; shipmentStatus?: { name: string; code: string; badge: string; }; paymentStatus?: { name: string; code: string; badge: string; }; items: OrderItem[]; totalQty: number; totalWeight: { value: number; unit: string; }; taxAmount: { value: number; text: string; }; totalTaxAmount: { value: number; text: string; }; taxAmountBeforeDiscount: { value: number; text: string; }; discountAmount: { value: number; text: string; }; shippingFeeExclTax: { value: number; text: string; }; shippingFeeInclTax: { value: number; text: string; }; shippingTaxAmount: { value: number; text: string; }; subTotal: { value: number; text: string; }; subTotalInclTax: { value: number; text: string; }; subTotalWithDiscount: { value: number; text: string; }; subTotalWithDiscountInclTax: { value: number; text: string; }; grandTotal: { value: number; text: string; }; billingAddress?: CustomerAddressGraphql; shippingAddress?: CustomerAddressGraphql; createdAt: { value: string; text: string; }; updatedAt: { value: string; text: string; }; } interface Customer { uuid: string; email: string; fullName: string; groupId: string; addresses: ExtendedCustomerAddress[]; orders: Order[]; addAddressApi: string; createdAt: { value: string; text: string; }; [key: string]: unknown; } interface CustomerState { customer: Customer | undefined; // undefined for guest users isLoading: boolean; } type CustomerAction = | { type: 'SET_LOADING'; payload: boolean } | { type: 'SET_CUSTOMER'; payload: Customer | undefined } | { type: 'LOGOUT' }; const initialState: CustomerState = { customer: undefined, isLoading: false }; const customerReducer = ( state: CustomerState, action: CustomerAction ): CustomerState => { return produce(state, (draft) => { switch (action.type) { case 'SET_LOADING': draft.isLoading = action.payload; break; case 'SET_CUSTOMER': draft.customer = action.payload; draft.isLoading = false; break; case 'LOGOUT': draft.customer = undefined; draft.isLoading = false; break; } }); }; interface CustomerContextValue extends CustomerState {} interface CustomerDispatchContextValue { login: ( data: { email: string; password: string; [key: string]: unknown; }, redirectUrl: string ) => Promise; register: ( data: { full_name: string; email: string; password: string; [key: string]: unknown; }, loginIfSuccess: boolean, redirectUrl: string ) => Promise; logout: () => Promise; setCustomer: (customer: Customer | undefined) => void; addAddress: ( addressData: Omit ) => Promise; updateAddress: ( addressId: string | number, addressData: Partial ) => Promise; deleteAddress: (addressId: string | number) => Promise; } const CustomerContext = createContext( undefined ); const CustomerDispatchContext = createContext< CustomerDispatchContextValue | undefined >(undefined); interface CustomerProviderProps { children: ReactNode; loginAPI: string; logoutAPI: string; registerAPI: string; initialCustomer?: Customer; } const retry = async ( fn: () => Promise, retries = 3, delay = 1000 ): Promise => { try { return await fn(); } catch (error) { if (retries > 0) { await new Promise((resolve) => setTimeout(resolve, delay)); return retry(fn, retries - 1, delay * 2); } throw error; } }; export function CustomerProvider({ children, loginAPI, registerAPI, logoutAPI, initialCustomer }: CustomerProviderProps) { const [state, dispatch] = useReducer(customerReducer, { ...initialState, customer: initialCustomer }); const appDispatch = useAppDispatch(); // Effect to update customer when initialCustomer prop changes useEffect(() => { // Compare by JSON string to handle object changes properly const currentCustomerStr = JSON.stringify(state.customer); const initialCustomerStr = JSON.stringify(initialCustomer); if (initialCustomerStr !== currentCustomerStr) { dispatch({ type: 'SET_CUSTOMER', payload: initialCustomer }); } }, [initialCustomer]); // Helper function to get current URL with isAjax=true const getCurrentAjaxUrl = useCallback(() => { const currentUrl = new URL(window.location.href); currentUrl.searchParams.set('ajax', 'true'); return currentUrl.toString(); }, []); // Login function const login = useCallback( async ( data: { email: string; password: string; [key: string]: unknown; }, redirectUrl: string ): Promise => { dispatch({ type: 'SET_LOADING', payload: true }); try { const response = await retry(() => fetch(loginAPI, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) ); const json = await response.json(); if (!response.ok) { throw new Error(json.error?.message || _('Login failed')); } // Trigger page data refresh which will update customer via useEffect await appDispatch.fetchPageData(getCurrentAjaxUrl()); if (redirectUrl) { window.location.href = redirectUrl; } return true; } catch (error) { dispatch({ type: 'SET_LOADING', payload: false }); throw error; } }, [loginAPI, appDispatch, getCurrentAjaxUrl] ); const register = useCallback( async ( data: { full_name: string; email: string; password: string; [key: string]: unknown; }, loginIfSuccess: boolean, redirectUrl: string ): Promise => { if (state.customer) { throw new Error(_('You are already logged in')); } dispatch({ type: 'SET_LOADING', payload: true }); try { const response = await retry(() => fetch(registerAPI, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) ); const json = await response.json(); if (!response.ok) { throw new Error(json.error?.message || _('Registration failed')); } // Trigger page data refresh which will update customer via useEffect await appDispatch.fetchPageData(getCurrentAjaxUrl()); if (loginIfSuccess) { // Auto login after successful registration await login( { email: data.email, password: data.password }, redirectUrl ); } return true; } catch (error) { dispatch({ type: 'SET_LOADING', payload: false }); throw error; } }, [registerAPI, appDispatch, getCurrentAjaxUrl, login] ); // Logout function const logout = useCallback(async (): Promise => { dispatch({ type: 'SET_LOADING', payload: true }); try { await retry(() => fetch(logoutAPI, { method: 'POST', headers: { 'Content-Type': 'application/json' } }) ); // After successful logout, clear customer data locally dispatch({ type: 'LOGOUT' }); } catch (error) { // Even if logout API fails, clear local customer data dispatch({ type: 'LOGOUT' }); throw error; } finally { dispatch({ type: 'SET_LOADING', payload: false }); } }, [logoutAPI]); // Set customer directly (for external updates) const setCustomer = useCallback((customer: Customer | undefined) => { dispatch({ type: 'SET_CUSTOMER', payload: customer }); }, []); // Add address function const addAddress = useCallback( async ( addressData: Omit ): Promise => { if (!state.customer?.addAddressApi) { throw new Error(_('Add address API not available')); } dispatch({ type: 'SET_LOADING', payload: true }); const response = await retry(() => fetch(state.customer!.addAddressApi!, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(addressData) }) ); const json = await response.json(); if (!response.ok) { throw new Error(json.error?.message || _('Failed to add address')); } if (json.error) { throw new Error(json.error.message || _('Failed to add address')); } // Sync with server to get fresh customer data including the new address await appDispatch.fetchPageData(getCurrentAjaxUrl()); // Return the address from the API response for immediate use const newAddress = json.data; if (!newAddress) { throw new Error(_('No address data received')); } return newAddress; }, [state.customer, appDispatch, getCurrentAjaxUrl] ); // Update address function const updateAddress = useCallback( async ( addressId: string | number, addressData: Partial ): Promise => { const address = state.customer?.addresses?.find( (addr) => addr.addressId === addressId ); if (!address?.updateApi) { throw new Error(_('Update address API not available')); } dispatch({ type: 'SET_LOADING', payload: true }); const response = await retry(() => fetch(address.updateApi!, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(addressData) }) ); const json = await response.json(); if (!response.ok) { throw new Error(json.error?.message || _('Failed to update address')); } if (json.error) { throw new Error(json.error.message || _('Failed to update address')); } // Sync with server to get fresh customer data including the updated address await appDispatch.fetchPageData(getCurrentAjaxUrl()); // Return the address from the API response for immediate use const updatedAddress = json.data; if (!updatedAddress) { throw new Error(_('No address data received')); } return updatedAddress; }, [state.customer, appDispatch, getCurrentAjaxUrl] ); // Delete address function const deleteAddress = useCallback( async (addressId: string | number): Promise => { const address = state.customer?.addresses?.find( (addr) => addr.addressId === addressId ); if (!address?.deleteApi) { throw new Error(_('Delete address API not available')); } dispatch({ type: 'SET_LOADING', payload: true }); const response = await retry(() => fetch(address.deleteApi!, { method: 'DELETE', headers: { 'Content-Type': 'application/json' } }) ); const json = await response.json(); if (!response.ok) { throw new Error(json.error?.message || _('Failed to delete address')); } if (json.error) { throw new Error(json.error.message || _('Failed to delete address')); } await appDispatch.fetchPageData(getCurrentAjaxUrl()); }, [state.customer, appDispatch, getCurrentAjaxUrl] ); const contextValue = useMemo( (): CustomerContextValue => ({ ...state }), [state] ); const dispatchMethods = useMemo( (): CustomerDispatchContextValue => ({ login, register, logout, setCustomer, addAddress, updateAddress, deleteAddress }), [login, logout, setCustomer, addAddress, updateAddress, deleteAddress] ); return ( {children} ); } export const useCustomer = (): CustomerContextValue => { const context = useContext(CustomerContext); if (context === undefined) { throw new Error('useCustomer must be used within a CustomerProvider'); } return context; }; export const useCustomerDispatch = (): CustomerDispatchContextValue => { const context = useContext(CustomerDispatchContext); if (context === undefined) { throw new Error( 'useCustomerDispatch must be used within a CustomerProvider' ); } return context; }; export type { Customer, CustomerState, ExtendedCustomerAddress }; ================================================ FILE: packages/evershop/src/components/frontStore/customer/LoginForm.tsx ================================================ import Area from '@components/common/Area.js'; import { Form, useFormContext } from '@components/common/form/Form.js'; import { InputField } from '@components/common/form/InputField.js'; import { PasswordField } from '@components/common/form/PasswordField.js'; import { Button } from '@components/common/ui/Button.js'; import { useCustomerDispatch } from '@components/frontStore/customer/CustomerContext.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import { cn } from '@evershop/evershop/lib/util/cn'; import { LockKeyhole, Mail } from 'lucide-react'; import React from 'react'; const SubmitButton: React.FC<{ formId: string }> = ({ formId }) => { const { formState: { isSubmitting } } = useFormContext(); return (
    ); }; export const CustomerLoginForm: React.FC<{ title?: string; subtitle?: string; redirectUrl: string; onError?: (error: any) => void; className?: string; }> = ({ title, subtitle, redirectUrl, onError, className }) => { const { login } = useCustomerDispatch(); return (
    {title && (

    {_(title)}

    )} {subtitle && (

    {_(subtitle)}

    )}
    { try { await login( { email: data.email, password: data.password, ...data }, redirectUrl ); } catch (error) { onError?.(error); } }} onError={onError} submitBtn={false} > } label={_('Email')} name="email" placeholder={_('Email')} required validation={{ required: _('Email is required') }} /> ) }, sortOrder: 10 }, { component: { default: ( } label={_('Password')} name="password" placeholder={_('Password')} required validation={{ required: _('Password is required') }} showToggle /> ) }, sortOrder: 20 }, { component: { default: }, sortOrder: 30 } ]} />
    ); }; ================================================ FILE: packages/evershop/src/components/frontStore/customer/MyAddresses.tsx ================================================ import { AddressSummary } from '@components/common/customer/address/AddressSummary.jsx'; import { CheckboxField } from '@components/common/form/CheckboxField.js'; import { Form } from '@components/common/form/Form.js'; import { Button } from '@components/common/ui/Button.js'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@components/common/ui/Dialog.js'; import { Item, ItemActions, ItemContent } from '@components/common/ui/Item.js'; import CustomerAddressForm from '@components/frontStore/customer/address/addressForm/Index.js'; import { ExtendedCustomerAddress, useCustomer, useCustomerDispatch } from '@components/frontStore/customer/CustomerContext.jsx'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; import { toast } from 'react-toastify'; const Address: React.FC<{ address: ExtendedCustomerAddress; }> = ({ address }) => { const { updateAddress, deleteAddress } = useCustomerDispatch(); const [dialogOpen, setDialogOpen] = React.useState(false); const classes = address.isDefault ? 'border-2 border-primary' : ''; return (
    {_('Edit Address')}
    { try { await updateAddress(address.addressId, data); setDialogOpen(false); toast.success(_('Address has been updated successfully!')); } catch (error) { toast.error(error.message); } }} >
    ); }; export function MyAddresses({ title }: { title?: string }) { const { customer } = useCustomer(); const { addAddress } = useCustomerDispatch(); const [dialogOpen, setDialogOpen] = React.useState(false); if (!customer) { return null; } return (
    {title && (

    {_('Address Book')}

    )} {customer.addresses.length === 0 && (
    {_('You have no addresses saved')}
    )}
    {customer.addresses.map((address) => (
    ))}
    {_('Add new address')}
    { try { await addAddress(data as ExtendedCustomerAddress); setDialogOpen(false); toast.success(_('Address has been saved successfully!')); } catch (error) { toast.error(error.message); } }} >
    ); } ================================================ FILE: packages/evershop/src/components/frontStore/customer/OrderHistory.tsx ================================================ import { Image } from '@components/common/Image.js'; import { ProductNoThumbnail } from '@components/common/ProductNoThumbnail.js'; import { Order, useCustomer } from '@components/frontStore/customer/CustomerContext.jsx'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; const OrderDetail: React.FC<{ order: Order }> = ({ order }) => { return (
    {order.items.map((item) => (
    {item.thumbnail && ( {item.productName} )} {!item.thumbnail && ( )}
    {item.productName}
    {_('Sku')}: #{item.productSku}
    {item.qty} x {item.productPrice.text}
    ))}
    {_('Order')}: #{order.orderNumber} {order.createdAt.text}
    {_('Total')}:{order.grandTotal.text}
    ); }; export default function OrderHistory({ title }: { title?: string }) { const { customer } = useCustomer(); const orders = customer?.orders || []; return (
    {title &&

    {title}

    } {orders.length === 0 && (
    {_('You have not placed any orders yet')}
    )} {orders.map((order) => (
    ))}
    ); } ================================================ FILE: packages/evershop/src/components/frontStore/customer/RegistrationForm.tsx ================================================ import { EmailField } from '@components/common/form/EmailField.js'; import { Form, useFormContext } from '@components/common/form/Form.js'; import { InputField } from '@components/common/form/InputField.js'; import { PasswordField } from '@components/common/form/PasswordField.js'; import { Area } from '@components/common/index.js'; import { Button } from '@components/common/ui/Button.js'; import { useCustomerDispatch } from '@components/frontStore/customer/CustomerContext.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import { LockKeyhole, Mail, User } from 'lucide-react'; import React from 'react'; const SubmitButton: React.FC<{ formId: string }> = ({ formId }) => { const { formState: { isSubmitting } } = useFormContext(); return (
    ); }; export const CustomerRegistrationForm: React.FC<{ title?: string; subtitle?: string; className?: string; redirectUrl: string; onError?: (error: string) => void; }> = ({ title, subtitle, redirectUrl, onError, className }) => { const { register } = useCustomerDispatch(); return (
    {title && (

    {_(title)}

    )} {subtitle && (

    {_(subtitle)}

    )}
    { try { await register( { full_name: data.full_name, email: data.email, password: data.password, ...data }, true, redirectUrl ); } catch (error) { onError?.(error.message); } }} submitBtn={false} > } name="full_name" label={_('Full Name')} placeholder={_('Full Name')} required validation={{ required: _('Full Name is required') }} /> ) }, sortOrder: 10 }, { component: { default: ( } name="email" label={_('Email')} placeholder={_('Email')} required validation={{ required: _('Email is required') }} /> ) }, sortOrder: 20 }, { component: { default: ( } name="password" label={_('Password')} placeholder={_('Password')} required showToggle validation={{ required: _('Password is required') }} /> ) }, sortOrder: 30 }, { component: { default: }, sortOrder: 30 } ]} />
    ); }; ================================================ FILE: packages/evershop/src/components/frontStore/customer/ResetPasswordForm.tsx ================================================ import { EmailField } from '@components/common/form/EmailField.js'; import { Form } from '@components/common/form/Form.js'; import { Button } from '@components/common/ui/Button.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import { Mail } from 'lucide-react'; import React from 'react'; import { useForm } from 'react-hook-form'; export const ResetPasswordForm: React.FC<{ title: string; subtitle: string; action: string; className: string; onSuccess: () => void; }> = ({ title, subtitle, action, className, onSuccess }) => { const [error, setError] = React.useState(null); const form = useForm(); const { formState: { isSubmitting: loading } } = form; return (
    {title && (

    {title}

    )} {subtitle && (

    {subtitle}

    )} {error &&
    {error}
    }
    { if (!response.error) { onSuccess(); } else { setError(response.error.message); } }} submitBtn={false} > } name="email" label={_('Email')} placeholder={_('Email')} required validation={{ required: _('Email is required') }} />
    ); }; ================================================ FILE: packages/evershop/src/components/frontStore/customer/address/addressForm/AddressForm.tsx ================================================ import Area from '@components/common/Area.js'; import { InputField } from '@components/common/form/InputField.js'; import { SelectField } from '@components/common/form/SelectField.js'; import { NameAndTelephone } from '@components/frontStore/customer/address/addressForm/NameAndTelephone.js'; import { ProvinceAndPostcode } from '@components/frontStore/customer/address/addressForm/ProvinceAndPostcode.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import { CustomerAddressGraphql } from '@evershop/evershop/types/customerAddress'; import React from 'react'; import { useFormContext } from 'react-hook-form'; interface CustomerAddressFormProps { allowCountries: { value: string; label: string; provinces: { value: string; label: string; }[]; }[]; address?: CustomerAddressGraphql; areaId?: string; fieldNamePrefix?: string; } export function CustomerAddressForm({ allowCountries = [], address = {}, areaId = 'customerAddressForm', fieldNamePrefix = 'address' }: CustomerAddressFormProps) { const { watch, setValue } = useFormContext(); const getFieldName = (fieldName: string) => { return fieldNamePrefix ? `${fieldNamePrefix}.${fieldName}` : fieldName; }; const selectedCountry = watch( getFieldName('country'), address?.country?.code || '' ); return ( ) }, sortOrder: 10 }, { component: { default: ( ) }, sortOrder: 20 }, { component: { default: ( ) }, sortOrder: 30 }, { component: { default: ( ) }, sortOrder: 40 }, { component: { default: ( { setValue(getFieldName('country'), value); setValue(getFieldName('province'), ''); }} required validation={{ required: _('Country is required') }} options={allowCountries} /> ) }, sortOrder: 50 }, { component: { default: ( country.value === selectedCountry )?.provinces || [] } province={address?.province || { code: '' }} postcode={address?.postcode || ''} getFieldName={getFieldName} /> ) }, sortOrder: 60 } ]} /> ); } ================================================ FILE: packages/evershop/src/components/frontStore/customer/address/addressForm/AddressFormLoadingSkeleton.scss ================================================ .address-loading-skeleton { width: 100%; display: flex; justify-content: center; flex-direction: column; padding: 20px; box-sizing: border-box; .skeleton:empty { width: 100%; height: 45px; margin-top: 10px; cursor: progress; background: linear-gradient(0.25turn, transparent, #fff, transparent), linear-gradient(#eee, #eee); background-repeat: no-repeat; animation: loading 1.5s infinite; border: 1px solid #eee; } @keyframes loading { to { background-position: 315px 0, 0 0, 0 190px, 50px 195px; } } } ================================================ FILE: packages/evershop/src/components/frontStore/customer/address/addressForm/AddressFormLoadingSkeleton.tsx ================================================ import React from 'react'; import './AddressFormLoadingSkeleton.scss'; export function AddressFormLoadingSkeleton() { return (
    ); } ================================================ FILE: packages/evershop/src/components/frontStore/customer/address/addressForm/Index.tsx ================================================ import { CustomerAddressForm } from '@components/frontStore/customer/address/addressForm/AddressForm.js'; import { AddressFormLoadingSkeleton } from '@components/frontStore/customer/address/addressForm/AddressFormLoadingSkeleton.js'; import { CustomerAddressGraphql } from '@evershop/evershop/types/customerAddress'; import React from 'react'; import { useQuery } from 'urql'; const CountriesQuery = ` query Country { allowedCountries { value: code label: name provinces { label: name value: code } } } `; interface IndexProps { address?: CustomerAddressGraphql; areaId?: string; fieldNamePrefix?: string; } export default function Index({ address = {}, areaId = 'customerAddressForm', fieldNamePrefix = 'address' }: IndexProps) { const [result] = useQuery({ query: CountriesQuery }); const { data, fetching, error } = result; if (fetching) return ; if (error) { return

    {error.message}

    ; } return ( ); } ================================================ FILE: packages/evershop/src/components/frontStore/customer/address/addressForm/NameAndTelephone.tsx ================================================ import { InputField } from '@components/common/form/InputField.js'; import { TelField } from '@components/common/form/TelField.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; interface NameAndTelephoneProps { fullName?: string; telephone?: string; getFieldName?: (fieldName: string) => string; } export function NameAndTelephone({ fullName, telephone, getFieldName }: NameAndTelephoneProps) { return (
    ); } ================================================ FILE: packages/evershop/src/components/frontStore/customer/address/addressForm/ProvinceAndPostcode.tsx ================================================ import { InputField } from '@components/common/form/InputField.js'; import { SelectField } from '@components/common/form/SelectField.js'; import { _ } from '@evershop/evershop/lib/locale/translate/_'; import React from 'react'; interface ProvinceAndPostcodeProps { provinces: { value: string; label: string; }[]; province?: { code: string; }; postcode?: string; getFieldName?: (fieldName: string) => string; } export function ProvinceAndPostcode({ provinces, province, postcode, getFieldName }: ProvinceAndPostcodeProps) { return (
    ); } ================================================ FILE: packages/evershop/src/lib/babel/config.js ================================================ module.exports = { parserOpts: { allowReturnOutsideFunction: true }, presets: [ '@babel/preset-react', [ '@babel/preset-env', { exclude: [ '@babel/plugin-transform-regenerator', '@babel/plugin-transform-async-to-generator' ] } ] ], ignore: ['node_modules'] }; ================================================ FILE: packages/evershop/src/lib/babel/index.js ================================================ import config from './config.js'; import '@babel/register'; config(); ================================================ FILE: packages/evershop/src/lib/componee/getComponentsByRoute.ts ================================================ import { resolve } from 'path'; import { getEnabledExtensions } from '../../bin/extension/index.js'; import { getCoreModules } from '../../bin/lib/loadModules.js'; import { getRoutes } from '../router/Router.js'; import { getEnabledTheme } from '../util/getEnabledTheme.js'; import { getEnabledWidgets } from '../widget/widgetManager.js'; import { ComponentsMap, scanRouteComponents } from './scanForComponents.js'; export function getComponentsByRoute(route) { const modules = [...getCoreModules(), ...getEnabledExtensions()]; const theme = getEnabledTheme(); let components; if (theme) { components = Object.values( scanRouteComponents(route, modules, resolve(theme.path, 'dist')) ); } else { components = Object.values(scanRouteComponents(route, modules)); } const widgets = getEnabledWidgets(); if (!route.isAdmin) { // Add widgets to components return components.concat((widgets || []).map((widget) => widget.component)); } else { // Add widgets to components return components.concat( (widgets || []).map((widget) => widget.settingComponent) ); } } interface AllRouteComponentsMap { [routeId: string]: ComponentsMap; } /** * Scan components for all routes * @returns A map of route IDs to their components */ export function getAllRouteComponents(isAdmin = false): AllRouteComponentsMap { const allComponents: AllRouteComponentsMap = {}; const routes = getRoutes().filter( (route) => route.isApi === false && route.isAdmin === isAdmin ); routes.forEach((route) => { allComponents[route.id] = getComponentsByRoute(route); }); return allComponents; } ================================================ FILE: packages/evershop/src/lib/componee/scanForComponents.ts ================================================ import { existsSync, readdirSync } from 'fs'; import { resolve, sep } from 'path'; import { Route } from '../../types/route.js'; interface ComponentsMap { [key: string]: string; } function scanForComponents(path: string): string[] { return readdirSync(resolve(path), { withFileTypes: true }) .filter( (dirent) => dirent.isFile() && /.js$/.test(dirent.name) && /^[A-Z]/.test(dirent.name[0]) ) .map((dirent) => resolve(path, dirent.name)); } function scanRouteComponents( route: Route, modules: { name: string; path: string; }[], themePath: string | null = null ): ComponentsMap { let components: ComponentsMap = {}; modules.forEach((module) => { // Scan for 'all' components const rootPath = route.isAdmin ? resolve(module.path, 'pages/admin') : resolve(module.path, 'pages/frontStore'); // Get all folders in the rootPath const pages = existsSync(rootPath) ? readdirSync(rootPath, { withFileTypes: true }) .filter((dirent) => dirent.isDirectory()) .map((dirent) => dirent.name) : []; pages.forEach((page) => { let moduleComponents: string[] = []; if (page === 'all' || page === route.id) { moduleComponents = [ ...moduleComponents, ...scanForComponents(resolve(rootPath, page)) ]; } // Check if page include `+ page` or `page+` in the name if (page.includes('+') && page.includes(route.id)) { moduleComponents = [ ...moduleComponents, ...scanForComponents(resolve(rootPath, page)) ]; } const componentsObject = moduleComponents.reduce( (a: ComponentsMap, v: string) => { // Split the path by separator and get the 2 last items (routeId and component name) const key = v.split(sep).slice(-2).join('/'); return { ...a, [key]: v }; }, {} ); components = { ...components, ...componentsObject }; }); }); // Scan for theme components, only support frontStore theme if (!route.isAdmin && themePath) { const themePages = existsSync(resolve(themePath, 'pages')) ? readdirSync(resolve(themePath, 'pages'), { withFileTypes: true }) .filter((dirent) => dirent.isDirectory()) .map((dirent) => dirent.name) : []; themePages.forEach((page) => { let themeComponents: string[] = []; if (page === 'all' || page === route.id) { themeComponents = [ ...themeComponents, ...scanForComponents(resolve(themePath, 'pages', page)) ]; } // Check if page include `+ page` or `page+` in the name if (page.includes('+') && page.includes(route.id)) { themeComponents = [ ...themeComponents, ...scanForComponents(resolve(themePath, 'pages', page)) ]; } const themeComponentsObject = themeComponents.reduce( (a: ComponentsMap, v: string) => { // Split the path by separator and get the 2 last items (routeId and component name) const key = v.split(sep).slice(-2).join('/'); return { ...a, [key]: v }; }, {} ); components = { ...components, ...themeComponentsObject }; }); } return components; } export { scanForComponents, scanRouteComponents }; export type { ComponentsMap }; ================================================ FILE: packages/evershop/src/lib/componee/scanForRootComponents.ts ================================================ import { join, normalize } from 'path'; import fg from 'fast-glob'; import { getEnabledExtensions } from '../../bin/extension/index.js'; import { CONSTANTS } from '../helpers.js'; import { getConfig } from '../util/getConfig.js'; /** * Scans for files matching a glob pattern within a list of specified locations. * This function is optimized for speed by using fast-glob. * * @param {function} callback - Optional callback to process the found files. * @returns {Promise} A promise that resolves to an array of absolute file paths. */ export async function scanForRootComponents(callback): Promise { const pattern = '/[A-Z]*.js'; const extensions = getEnabledExtensions(); const locations = [join(CONSTANTS.MODULESPATH, '/*/pages/*/*/')]; for (const ext of extensions) { locations.push(join(ext.path, 'pages/*/*/')); } const theme = getConfig('system.theme') as string | undefined; if (theme) { locations.push(join(CONSTANTS.ROOTPATH, `/themes/${theme}/dist/pages/*/`)); } const normalizedLocations = locations.map((loc) => normalize(loc)); const globPatterns = normalizedLocations.map((loc) => `${loc}/${pattern}`); const files = await fg(globPatterns, { absolute: true, onlyFiles: true, unique: true }); if (callback) { await callback(files); } return files; } ================================================ FILE: packages/evershop/src/lib/componee/tests/unit/__mocks__/modules/firstModule/pages/frontStore/all/Menu.js ================================================ ================================================ FILE: packages/evershop/src/lib/componee/tests/unit/__mocks__/modules/firstModule/pages/frontStore/productView/Name.js ================================================ ================================================ FILE: packages/evershop/src/lib/componee/tests/unit/__mocks__/modules/firstModule/pages/frontStore/productView/Price.js ================================================ ================================================ FILE: packages/evershop/src/lib/componee/tests/unit/__mocks__/modules/secondModule/pages/frontStore/all/Banner.js ================================================ ================================================ FILE: packages/evershop/src/lib/componee/tests/unit/__mocks__/modules/secondModule/pages/frontStore/productView/Description.js ================================================ ================================================ FILE: packages/evershop/src/lib/componee/tests/unit/__mocks__/modules/secondModule/pages/frontStore/productView/Inventory.js ================================================ ================================================ FILE: packages/evershop/src/lib/componee/tests/unit/__mocks__/modules/secondModule/pages/frontStore/productView/Name.js ================================================ ================================================ FILE: packages/evershop/src/lib/componee/tests/unit/__mocks__/themes/justatheme/pages/all/CommentList.js ================================================ ================================================ FILE: packages/evershop/src/lib/componee/tests/unit/__mocks__/themes/justatheme/pages/all/Shipping.js ================================================ ================================================ FILE: packages/evershop/src/lib/componee/tests/unit/__mocks__/themes/justatheme/pages/productView/Name.js ================================================ ================================================ FILE: packages/evershop/src/lib/componee/tests/unit/__mocks__/themes/justatheme/pages/productView/OutOfStock.js ================================================ ================================================ FILE: packages/evershop/src/lib/componee/tests/unit/__mocks__/themes/justatheme/pages/productView/Price.js ================================================ ================================================ FILE: packages/evershop/src/lib/componee/tests/unit/scanRouteComponents.test.js ================================================ import path from 'path'; import { scanRouteComponents } from '../../scanForComponents.js'; import { jest, describe, it, expect, beforeAll, afterAll } from '@jest/globals'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); describe('test scanRouteComponents function', () => { const modules = [ { path: path.resolve(__dirname, './__mocks__/modules/firstModule'), name: 'firstModule' }, { path: path.resolve(__dirname, './__mocks__/modules/secondModule'), name: 'secondModule' } ]; const extensions = [ { path: path.resolve(__dirname, './__mocks__/extensions/secondExtension'), name: 'secondExtension', priority: 2 }, { path: path.resolve(__dirname, './__mocks__/extensions/firstExtension'), name: 'firstExtension', priority: 1 } ]; const themePath = path.resolve(__dirname, './__mocks__/themes/justathemes'); it('It should return an object', () => { const components = scanRouteComponents( { id: 'home', isAdmin: false }, [...modules, ...extensions], themePath ); expect(components).toBeInstanceOf(Object); }); it('It should get only `all` component if route does not exist', () => { const components = scanRouteComponents( { id: 'home', isAdmin: false }, [...modules, ...extensions], themePath ); expect(components).toEqual({ 'all/Menu.js': path.resolve( __dirname, './__mocks__/modules/firstModule/pages/frontStore/all/Menu.js' ), 'all/Banner.js': path.resolve( __dirname, './__mocks__/extensions/firstExtension/pages/frontStore/all/Banner.js' ), 'all/CommentList.js': path.resolve( __dirname, './__mocks__/extensions/firstExtension/pages/frontStore/all/CommentList.js' ) }); }); it('It should get the component from extension when the component is dublicated with the one in core module', () => { const components = scanRouteComponents( { id: 'productView', isAdmin: false }, [...modules, ...extensions], themePath ); expect(components['productView/Price.js']).toEqual( path.resolve( __dirname, './__mocks__/extensions/firstExtension/pages/frontStore/productView/Price.js' ) ); }); it('It should get the component from higher priority extension when the component is dublicated', () => { const components = scanRouteComponents( { id: 'productView', isAdmin: false }, [...modules, ...extensions], themePath ); expect(components['productView/Name.js']).toEqual( path.resolve( __dirname, './__mocks__/extensions/firstExtension/pages/frontStore/productView/Name.js' ) ); }); it('It should get the components theme, theme should be higest priority', () => { const components = scanRouteComponents( { id: 'productView', isAdmin: false }, [...modules, ...extensions], path.resolve(__dirname, './__mocks__/themes/justatheme') ); expect(components['all/Shipping.js']).toEqual( path.resolve( __dirname, './__mocks__/themes/justatheme/pages/all/Shipping.js' ) ); expect(components['all/CommentList.js']).toEqual( path.resolve( __dirname, './__mocks__/themes/justatheme/pages/all/CommentList.js' ) ); expect(components['productView/Name.js']).toEqual( path.resolve( __dirname, './__mocks__/themes/justatheme/pages/productView/Name.js' ) ); expect(components['productView/Price.js']).toEqual( path.resolve( __dirname, './__mocks__/themes/justatheme/pages/productView/Price.js' ) ); expect(components['productView/OutOfStock.js']).toEqual( path.resolve( __dirname, './__mocks__/themes/justatheme/pages/productView/OutOfStock.js' ) ); }); }); ================================================ FILE: packages/evershop/src/lib/cronjob/cronjob.ts ================================================ import { pathToFileURL } from 'url'; import cron from 'node-cron'; import { getEnabledExtensions } from '../../bin/extension/index.js'; import { loadBootstrapScript } from '../../bin/lib/bootstrap/bootstrap.js'; import { getCoreModules } from '../../bin/lib/loadModules.js'; import { debug, error } from '../log/logger.js'; import { lockHooks } from '../util/hookable.js'; import { lockRegistry } from '../util/registry.js'; import { getEnabledJobs } from './jobManager.js'; async function start() { const modules = [...getCoreModules(), ...getEnabledExtensions()]; /** Loading bootstrap script from modules */ try { for (const module of modules) { await loadBootstrapScript(module, { ...JSON.parse(process.env.bootstrapContext || '{}'), process: 'cronjob' }); } lockHooks(); lockRegistry(); } catch (e) { error(e); process.exit(0); } const jobs = getEnabledJobs(); // Schedule the jobs jobs.forEach((job) => { cron.schedule(job.schedule, async () => { try { // Load the module const module = await import(pathToFileURL(job.resolve).toString()); // Make sure the module is a function or async function if (typeof module.default !== 'function') { throw new Error( `Job ${job.name} is not a function. Make sure the module exports a function as default.` ); } // Execute the job await module.default(); } catch (e) { error(e); } }); }); } process.on('SIGTERM', async () => { debug('Cron job received SIGTERM, shutting down...'); try { process.exit(0); } catch (err) { error('Error during shutdown:'); error(err); process.exit(1); // Exit with an error code } }); process.on('SIGINT', async () => { debug('Cron job received SIGINT, shutting down...'); try { process.exit(0); } catch (err) { error('Error during shutdown:'); error(err); process.exit(1); // Exit with an error code } }); start(); ================================================ FILE: packages/evershop/src/lib/cronjob/jobManager.ts ================================================ import * as fs from 'fs'; import * as path from 'path'; import cron from 'node-cron'; import { Job } from '../../types/cronjob.js'; import { warning } from '../log/logger.js'; /** * Checks if a given path is a valid and resolvable JavaScript file path. * A path is considered valid if it's a string, not empty, exists on the filesystem, * and has a .js extension. * * @param {string | undefined} filePath - The path to check. Can be undefined. * @returns {boolean} True if the path is a resolvable JavaScript file, false otherwise. */ function isValidJsFilePath(filePath: string | undefined): boolean { if (typeof filePath !== 'string' || filePath.trim() === '') { return false; } const resolvedPath = path.resolve(filePath); const fileExtension = path.extname(resolvedPath); try { if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isFile()) { return false; } } catch (e) { return false; } return fileExtension === '.js'; } class JobManager { /** * @private * A private map to store registered jobs. The key is the job's unique name, * and the value is the job object adhering to the Job interface. */ private jobs: Map = new Map(); /** * @private * A flag indicating whether the job manager has entered a read-only state. * Once set to true (after `getAllJobs` is called for the first time), * no further mutations (add, remove, update) are allowed. */ private _isFrozen: boolean = false; /** * Internal helper to check if mutations are allowed. * Throws an error if the manager is in a frozen (read-only) state. * @private * @throws {Error} If a mutation attempt is made after the manager is frozen. */ private _ensureMutable(): void { if (this._isFrozen) { throw new Error( 'Job manager is in a read-only state. No further mutations are allowed. We suggest to register, update, or remove a job from the bootstrap file.' ); } } /** * Registers a new job with the manager. * A job must have a unique 'name' property. * If a job with the same name already exists, it will not be registered. * Additionally, `resolve` must be a resolvable path to a JavaScript file. * @param {Job} job - The job object to register. * @returns {boolean} True if the job was successfully registered, false otherwise. * @throws {Error} If called after the manager has entered a read-only state or on invalid job data/paths. */ public registerJob(job: Job): boolean { this._ensureMutable(); if (!job || typeof job.name !== 'string' || job.name.trim() === '') { throw new Error( 'Cannot register job. Job object must have a valid "name" property.' ); } const jobName = job.name; if (this.jobs.has(jobName)) { warning( `Job with name "${jobName}" is already registered. Skipping registration.` ); return false; } if (!isValidJsFilePath(job.resolve)) { throw new Error( `Cannot register job "${jobName}". Invalid or unresolvable path: "${job.resolve}". Please ensure it's a valid path to an existing JS file.` ); } if (!cron.validate(job.schedule)) { throw new Error( `Cannot register job "${jobName}". Invalid cron schedule: "${job.schedule}". Please ensure it's a valid cron expression.` ); } this.jobs.set(jobName, job); return true; } /** * Removes a job from the manager based on its unique name. * * @param {string} jobName - The name of the job to remove. * @returns {boolean} True if the job was successfully removed, false otherwise. * @throws {Error} If called after the manager has entered a read-only state or on invalid job name. */ public removeJob(jobName: string): boolean { this._ensureMutable(); if (this.jobs.has(jobName)) { this.jobs.delete(jobName); return true; } else { warning(`Job with name "${jobName}" not found. Cannot remove.`); return false; } } /** * Updates an existing job's schedule. This method allows you to change the cron schedule of a job. * @param {string} jobName - The name of the job to update. * @param {string} newSchedule - The new cron schedule to set. * @returns {boolean} True if the job was successfully updated, false otherwise. * @throws {Error} If called after the manager has entered a read-only state or on invalid job name. */ public updateJobSchedule(jobName: string, newSchedule: string): boolean { this._ensureMutable(); const job = this.jobs.get(jobName); if (!cron.validate(newSchedule)) { throw new Error( `Cannot update job "${jobName}". Invalid cron schedule: "${newSchedule}". Please ensure it's a valid cron expression.` ); } if (!job) { warning(`Job with name "${jobName}" not found. Cannot update schedule.`); return false; } job.schedule = newSchedule; return true; } /** * Retrieves a registered job by its unique name. * * @param {string} jobName - The name of the job to retrieve. * @returns {Job | undefined} The job object if found, otherwise undefined. */ public getJob(jobName: string): Job | undefined { if (this.jobs.has(jobName)) { return this.jobs.get(jobName); } else { warning(`Job with name "${jobName}" not found.`); return undefined; } } /** * Retrieves all registered jobs. * Returns a new array containing frozen (immutable) copies of the job objects. * This method also marks the JobManager as 'frozen', preventing any further * calls to mutation methods (register, remove, update). * * @returns {Job[]} An array containing all registered job objects. */ public getAllJobs(): Job[] { this._isFrozen = true; // Create a new array, and for each job, create a frozen copy. return Array.from(this.jobs.values()).map((job) => Object.freeze({ ...job }) ); } /** * Checks if a job with the given name is registered. * @param {string} jobName - The name of the job to check. * @returns {boolean} True if the job is registered, false otherwise. */ public hasJob(jobName: string): boolean { return this.jobs.has(jobName); } } const jobManager = new JobManager(); /** * Retrieves all registered jobs. * Calling this function will also freeze the job manager, preventing any further mutations (register, remove). * @returns {Job[]} An array of all registered jobs. */ export function getAllJobs(): Job[] { const allJobs = jobManager.getAllJobs(); return allJobs; } /** * Retrieves all enabled jobs. An enabled job is one that has its `enabled` property set to true. * This function returns a new array containing only the jobs that are enabled. Calling this function * will also freeze the job manager, preventing any further mutations (register, remove). * @returns {Job[]} An array of enabled jobs. */ export function getEnabledJobs(): Job[] { const allJobs = jobManager.getAllJobs(); return allJobs.filter((job) => job.enabled); } /** * Registers a new job. This function is intended to be called during the * bootstrap phase of the application, before the job manager is frozen. * @param job - The job object to register. * @returns True if the job was successfully registered, false otherwise. * @throws Error if the job is invalid or if the manager is in a read-only state. */ export function registerJob(job: Job): boolean { return jobManager.registerJob(job); } /** * Updates the schedule of an existing job. This function allows you to change * the cron schedule of a job. It is intended to be called during the bootstrap * phase of the application, before the job manager is frozen. * @param jobName - The name of the job to update. * @param newSchedule - The new cron schedule to set for the job. * @returns True if the job schedule was successfully updated, false otherwise. * @throws Error if the manager is in a read-only state. */ export function updateJobSchedule( jobName: string, newSchedule: string ): boolean { return jobManager.updateJobSchedule(jobName, newSchedule); } /** * Removes a job. This function supposed to be called from the bootstrap * phase of the application, before the job manager is frozen. * @param jobName - The name of the job to remove. * @returns True if the job was successfully removed, false otherwise. */ export function removeJob(jobName: string): boolean { return jobManager.removeJob(jobName); } /** * Retrieves a job by its name. * @param jobName - The name of the job to retrieve. * @returns The job if found, undefined otherwise. */ export function getJob(jobName: string): Job | undefined { return jobManager.getJob(jobName); } /** * Checks if a job with the given name is registered. * @param jobName - The name of the job to check. * @returns True if the job is registered, false otherwise. */ export function hasJob(jobName: string): boolean { return jobManager.hasJob(jobName); } ================================================ FILE: packages/evershop/src/lib/cronjob/tests/unit/jobManager.test.js ================================================ import { jest, describe, it, expect, beforeAll, afterAll } from '@jest/globals'; const getValidJob = (name = 'TestJob') => ({ name: name, schedule: '* * * * *', resolve: `./jobs/${name}.js`, enabled: true }); jest.unstable_mockModule('fs', () => ({ existsSync: jest.fn((path) => { if (path.includes('InvalidModule.js')) { return false; // Simulate unresolvable paths } return true; // Simulate valid paths for other components }), statSync: jest.fn(() => ({ isFile: () => true })) })); const realPath = await import('path'); jest.unstable_mockModule('path', () => ({ default: true, ...realPath, resolve: jest.fn((...args) => `/mocked/path/${args.join('/')}`) })); describe('Job Manager Module', () => { beforeEach(async () => { jest.resetModules(); // Reset modules before each test to ensure fresh mocks }); afterEach(() => { jest.clearAllMocks(); // Clear mock call history after each test }); // --- Test _isFrozen state enforcement --- describe('Mutation after getAllJobs()', () => { it('should throw an error if resolve is not a string', async () => { const jobModule = await import('../../jobManager.js'); const invalidJob = { name: 'InvalidJob', schedule: '* * * * *', resolve: 123, // Invalid type enabled: true }; expect(() => jobModule.registerJob(invalidJob)).toThrow( 'Invalid or unresolvable' ); }); it('should throw an error if the resolve is unresolvable path', async () => { const jobModule = await import('../../jobManager.js'); const invalidJob = { name: 'InvalidJob', schedule: '* * * * *', resolve: 'InvalidModule.js', // Unresolvable path enabled: true }; expect(() => jobModule.registerJob(invalidJob)).toThrow( 'Invalid or unresolvable' ); }); it('should throw an error if registerJob is called with an invalid job schedule', async () => { const jobModule = await import('../../jobManager.js'); const invalidJob = { name: 'InvalidJob', schedule: 'invalid_schedule', // Invalid schedule resolve: 'ValidModule.js', enabled: true }; expect(() => jobModule.registerJob(invalidJob)).toThrow( 'Invalid cron schedule' ); }); it('should throw an error if registerJob is called after getAllJobs', async () => { const jobModule = await import('../../jobManager.js'); jobModule.getAllJobs(); // This freezes the manager const job = getValidJob('NewJob'); expect(() => jobModule.registerJob(job)).toThrow( 'Job manager is in a read-only state. No further mutations are allowed. We suggest to register, update, or remove a job from the bootstrap file.' ); }); it('should throw an error if updateJob is called after getAllJobs', async () => { const jobModule = await import('../../jobManager.js'); jobModule.registerJob(getValidJob('ExistingJob')); jobModule.getAllJobs(); // This freezes the manager expect(() => jobModule.updateJobSchedule('ExistingJob', '0 0 * * *') ).toThrow( 'Job manager is in a read-only state. No further mutations are allowed. We suggest to register, update, or remove a job from the bootstrap file.' ); }); it('should throw an error if removeJob is called after getAllJobs', async () => { const jobModule = await import('../../jobManager.js'); jobModule.registerJob(getValidJob('JobToRemove')); jobModule.getAllJobs(); // This freezes the manager expect(() => jobModule.removeJob('JobToRemove')).toThrow( 'Job manager is in a read-only state. No further mutations are allowed. We suggest to register, update, or remove a job from the bootstrap file.' ); }); it('should allow getJob after getAllJobs', async () => { const jobModule = await import('../../jobManager.js'); const job = getValidJob('ReadJob'); jobModule.registerJob(job); jobModule.getAllJobs(); // This freezes the manager expect(jobModule.getJob('ReadJob')).toEqual(job); }); it('should allow hasJob after getAllJobs', async () => { const jobModule = await import('../../jobManager.js'); const job = getValidJob('CheckJob'); jobModule.registerJob(job); jobModule.getAllJobs(); // This freezes the manager expect(jobModule.hasJob('CheckJob')).toBe(true); }); it('should allow to updateJobSchedule', async () => { const jobModule = await import('../../jobManager.js'); const job = getValidJob('UpdateJob'); jobModule.registerJob(job); jobModule.updateJobSchedule('UpdateJob', '0 0 * * *'); expect(jobModule.getJob('UpdateJob')).toEqual(job); expect(jobModule.getJob('UpdateJob').schedule).toEqual('0 0 * * *'); }); it('should throw error if trying to updateJobSchedule with invalid schedule', async () => { const jobModule = await import('../../jobManager.js'); jobModule.registerJob(getValidJob('JobWithInvalidSchedule')); expect(() => jobModule.updateJobSchedule( 'JobWithInvalidSchedule', 'invalid_schedule' ) ).toThrow('Invalid cron schedule'); }); it('should thrown an error if trying to update job schedule after calling getAllJobs', async () => { const jobModule = await import('../../jobManager.js'); jobModule.registerJob(getValidJob('JobToUpdate')); jobModule.getAllJobs(); // This freezes the manager expect(() => jobModule.updateJobSchedule('JobToUpdate', '0 0 * * *') ).toThrow( 'Job manager is in a read-only state. No further mutations are allowed. We suggest to register, update, or remove a job from the bootstrap file.' ); }); it('should allow removing a job', async () => { const jobModule = await import('../../jobManager.js'); const job = getValidJob('JobToRemove'); jobModule.registerJob(job); jobModule.removeJob('JobToRemove'); expect(jobModule.hasJob('JobToRemove')).toBe(false); }); }); }); ================================================ FILE: packages/evershop/src/lib/event/callSubscibers.js ================================================ import { error } from '../../lib/log/logger.js'; export async function callSubscribers(subscribers, eventData) { const promises = subscribers.map( (subscriber) => new Promise((resolve) => { setTimeout(async () => { try { await subscriber(eventData); } catch (e) { error(e); } resolve(); }, 0); }) ); await Promise.all(promises); } ================================================ FILE: packages/evershop/src/lib/event/emitter.ts ================================================ import { insert } from '@evershop/postgres-query-builder'; import { EventDataRegistry, EventName } from '../../types/event.js'; import { pool } from '../postgres/connection.js'; /** * Emit a typed event. The event data type is inferred from the event name. * @param name - The name of the event (must be registered in EventDataRegistry) * @param data - The data to emit (type is inferred from event name) */ export async function emit( name: T, data: EventDataRegistry[T] ): Promise; /** * Emit an untyped event. Use this for dynamic events that aren't registered. * @param name - The name of the event * @param data - The data to emit */ export async function emit( name: string, data: Record ): Promise; // Implementation export async function emit(name: string, data: Record) { await insert('event') .given({ name, data }) .execute(pool); } ================================================ FILE: packages/evershop/src/lib/event/event-manager.js ================================================ import { del, select } from '@evershop/postgres-query-builder'; import { getEnabledExtensions } from '../../bin/extension/index.js'; import { loadBootstrapScript } from '../../bin/lib/bootstrap/bootstrap.js'; import { getCoreModules } from '../../bin/lib/loadModules.js'; import { pool } from '../../lib/postgres/connection.js'; import { debug, error } from '../log/logger.js'; import { lockHooks } from '../util/hookable.js'; import { lockRegistry } from '../util/registry.js'; import { callSubscribers } from './callSubscibers.js'; import { loadSubscribers } from './loadSubscribers.js'; const loadEventInterval = 10000; const syncEventInterval = 2000; const maxEvents = 10; let events = []; // Get the modules from the arguments const modules = [...getCoreModules(), ...getEnabledExtensions()]; const subscribers = await loadSubscribers(modules); const init = async () => { /** Loading bootstrap script from modules */ try { for (const module of modules) { await loadBootstrapScript(module, { ...JSON.parse(process.env.bootstrapContext || '{}'), process: 'event' }); } lockHooks(); lockRegistry(); } catch (e) { error(e); process.exit(0); } process.env.ALLOW_CONFIG_MUTATIONS = false; setInterval(async () => { // Load events const newEvents = await loadEvents(maxEvents); // Append the new events to the existing events events = [...events, ...newEvents]; // Keep only the last 20 events events = events.slice(-maxEvents); // Call subscribers for each event events.forEach((event) => { if (event.status !== 'done' && event.status !== 'processing') { executeSubscribers(event); } }); }, loadEventInterval); }; setInterval(async () => { // Sync events await syncEvents(); }, syncEventInterval); async function loadEvents(count) { // Only load events if the current events are less than the max events if (events.length >= maxEvents) { return []; } // Only load events that have subscribers const eventNames = subscribers.map((subscriber) => subscriber.event); const query = select().from('event'); if (eventNames.length > 0) { query.where('name', 'IN', eventNames); } if (events.length > 0) { query.andWhere( 'uuid', 'NOT IN', events.map((event) => event.uuid) ); } query.orderBy('event_id', 'ASC'); query.limit(0, count); const results = await query.execute(pool); return results; } async function syncEvents() { // Delete the events that have been executed const completedEvents = events .filter((event) => event.status === 'done') .map((event) => event.uuid); if (completedEvents.length > 0) { await del('event').where('uuid', 'IN', completedEvents).execute(pool); // Remove the events from the events array events = events.filter((event) => event.status !== 'done'); } } async function executeSubscribers(event) { event.status = 'processing'; const eventData = event.data; // get subscribers for the event const matchingSubscribers = subscribers .filter((subscriber) => subscriber.event === event.name) .map((subscriber) => subscriber.subscriber); // Call subscribers await callSubscribers(matchingSubscribers, eventData); event.status = 'done'; } process.on('SIGTERM', async () => { debug('Event manager received SIGTERM, shutting down...'); try { process.exit(0); } catch (err) { error('Error during shutdown:'); error(err); process.exit(1); // Exit with an error code } }); process.on('SIGINT', async () => { debug('Event manager received SIGINT, shutting down...'); try { process.exit(0); } catch (err) { error('Error during shutdown:'); error(err); process.exit(1); // Exit with an error code } }); init(); ================================================ FILE: packages/evershop/src/lib/event/loadSubscribers.js ================================================ import fs from 'fs'; import path from 'path'; import { pathToFileURL } from 'url'; import { error } from '../../lib/log/logger.js'; async function loadModuleSubscribers(modulePath) { const subscribers = []; const subscribersDir = path.join(modulePath, 'subscribers'); if (!fs.existsSync(subscribersDir)) { return subscribers; } const eventDirs = fs .readdirSync(subscribersDir, { withFileTypes: true }) .filter((dirent) => dirent.isDirectory()) .map((dirent) => dirent.name); await Promise.all( eventDirs.map(async (eventName) => { const eventSubscribersDir = path.join(subscribersDir, eventName); // get only .js files const files = fs .readdirSync(eventSubscribersDir, { withFileTypes: true }) .filter((dirent) => dirent.isFile() && dirent.name.endsWith('.js')) .map((dirent) => dirent.name); await Promise.all( files.map(async (file) => { const subscriberPath = path.join(eventSubscribersDir, file); const module = await import(pathToFileURL(subscriberPath)); subscribers.push({ event: eventName, subscriber: module.default }); }) ); }) ); return subscribers; } export async function loadSubscribers(modules) { const subscribers = []; /** Loading subscriber */ await Promise.all( modules.map(async (module) => { try { // Load subscribers subscribers.push(...(await loadModuleSubscribers(module.path))); } catch (e) { error(e); process.exit(0); } }) ); return subscribers; } ================================================ FILE: packages/evershop/src/lib/event/subscriber.ts ================================================ import { EventDataRegistry, EventName } from '../../types/event.js'; /** * Type-safe event subscriber function. * Use this type to ensure your subscriber receives the correct event data type. * * @example * ```typescript * import { EventSubscriber } from '@evershop/evershop/lib/event/subscriber'; * * const handler: EventSubscriber<'order.placed'> = async (data) => { * // data is typed as EventDataRegistry['order.placed'] * console.log(data.orderId); // TypeScript knows this exists * }; * * export default handler; * ``` */ export type EventSubscriber = ( data: EventDataRegistry[T] ) => Promise | void; /** * Helper function to create a type-safe event subscriber. * Provides better IDE autocomplete and type checking. * * @example * ```typescript * import { createSubscriber } from '@evershop/evershop/lib/event/subscriber'; * * export default createSubscriber('order_placed', async (data) => { * // data is automatically typed * await sendEmail(data.orderId); * }); * ``` */ export function createSubscriber( eventName: T, handler: EventSubscriber ): (data: EventDataRegistry[T]) => Promise { return async (data: EventDataRegistry[T]) => { await handler(data); }; } ================================================ FILE: packages/evershop/src/lib/helpers.ts ================================================ import path from 'path'; import { fileURLToPath } from 'url'; import { getConfig } from './util/getConfig.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const rootPath = __dirname.includes( path.join('node_modules', '@evershop', 'evershop') ) ? process.cwd() : path.resolve(__dirname, '..', '..', '..', '..'); export const CONSTANTS = Object.freeze({ ROOTPATH: rootPath, LIBPATH: path.resolve(__dirname), MODULESPATH: path.resolve(__dirname, '..', 'modules'), PUBLICPATH: path.resolve(rootPath, 'public'), MEDIAPATH: path.resolve(rootPath, 'media'), NODEMODULEPATH: path.resolve(rootPath, 'node_modules'), THEMEPATH: path.resolve(rootPath, 'themes'), CACHEPATH: path.resolve(rootPath, '.evershop'), BUILDPATH: path.resolve(rootPath, '.evershop', 'build'), ADMIN_COLLECTION_SIZE: getConfig('system.admin_collection_size', 20) }); ================================================ FILE: packages/evershop/src/lib/locale/countries.ts ================================================ export interface Country { code: string; name: string; } export const countries: Country[] = [ { code: 'AF', name: 'Afghanistan' }, { code: 'AL', name: 'Albania' }, { code: 'DZ', name: 'Algeria' }, { code: 'AS', name: 'American Samoa' }, { code: 'AD', name: 'Andorra' }, { code: 'AO', name: 'Angola' }, { code: 'AI', name: 'Anguilla' }, { code: 'AQ', name: 'Antarctica' }, { code: 'AG', name: 'Antigua and Barbuda' }, { code: 'AR', name: 'Argentina' }, { code: 'AM', name: 'Armenia' }, { code: 'AW', name: 'Aruba' }, { code: 'AU', name: 'Australia' }, { code: 'AT', name: 'Austria' }, { code: 'AZ', name: 'Azerbaijan' }, { code: 'BS', name: 'Bahamas' }, { code: 'BH', name: 'Bahrain' }, { code: 'BD', name: 'Bangladesh' }, { code: 'BB', name: 'Barbados' }, { code: 'BY', name: 'Belarus' }, { code: 'BE', name: 'Belgium' }, { code: 'BZ', name: 'Belize' }, { code: 'BJ', name: 'Benin' }, { code: 'BM', name: 'Bermuda' }, { code: 'BT', name: 'Bhutan' }, { code: 'BO', name: 'Bolivia' }, { code: 'BA', name: 'Bosnia and Herzegovina' }, { code: 'BW', name: 'Botswana' }, { code: 'BV', name: 'Bouvet Island' }, { code: 'BR', name: 'Brazil' }, { code: 'IO', name: 'British Indian Ocean Territory' }, { code: 'VG', name: 'British Virgin Islands' }, { code: 'BN', name: 'Brunei' }, { code: 'BG', name: 'Bulgaria' }, { code: 'BF', name: 'Burkina Faso' }, { code: 'BI', name: 'Burundi' }, { code: 'KH', name: 'Cambodia' }, { code: 'CM', name: 'Cameroon' }, { code: 'CA', name: 'Canada' }, { code: 'CV', name: 'Cape Verde' }, { code: 'KY', name: 'Cayman Islands' }, { code: 'CF', name: 'Central African Republic' }, { code: 'TD', name: 'Chad' }, { code: 'CL', name: 'Chile' }, { code: 'CN', name: 'China' }, { code: 'CX', name: 'Christmas Island' }, { code: 'CC', name: 'Cocos [Keeling] Islands' }, { code: 'CO', name: 'Colombia' }, { code: 'KM', name: 'Comoros' }, { code: 'CG', name: 'Congo - Brazzaville' }, { code: 'CD', name: 'Congo - Kinshasa' }, { code: 'CK', name: 'Cook Islands' }, { code: 'CR', name: 'Costa Rica' }, { code: 'HR', name: 'Croatia' }, { code: 'CU', name: 'Cuba' }, { code: 'CY', name: 'Cyprus' }, { code: 'CZ', name: 'Czech Republic' }, { code: 'CI', name: 'Côte d’Ivoire' }, { code: 'DK', name: 'Denmark' }, { code: 'DJ', name: 'Djibouti' }, { code: 'DM', name: 'Dominica' }, { code: 'DO', name: 'Dominican Republic' }, { code: 'EC', name: 'Ecuador' }, { code: 'EG', name: 'Egypt' }, { code: 'SV', name: 'El Salvador' }, { code: 'GQ', name: 'Equatorial Guinea' }, { code: 'ER', name: 'Eritrea' }, { code: 'EE', name: 'Estonia' }, { code: 'ET', name: 'Ethiopia' }, { code: 'FK', name: 'Falkland Islands' }, { code: 'FO', name: 'Faroe Islands' }, { code: 'FJ', name: 'Fiji' }, { code: 'FI', name: 'Finland' }, { code: 'FR', name: 'France' }, { code: 'GF', name: 'French Guiana' }, { code: 'PF', name: 'French Polynesia' }, { code: 'TF', name: 'French Southern Territories' }, { code: 'GA', name: 'Gabon' }, { code: 'GM', name: 'Gambia' }, { code: 'GE', name: 'Georgia' }, { code: 'DE', name: 'Germany' }, { code: 'GH', name: 'Ghana' }, { code: 'GI', name: 'Gibraltar' }, { code: 'GR', name: 'Greece' }, { code: 'GL', name: 'Greenland' }, { code: 'GD', name: 'Grenada' }, { code: 'GP', name: 'Guadeloupe' }, { code: 'GU', name: 'Guam' }, { code: 'GT', name: 'Guatemala' }, { code: 'GG', name: 'Guernsey' }, { code: 'GN', name: 'Guinea' }, { code: 'GW', name: 'Guinea-Bissau' }, { code: 'GY', name: 'Guyana' }, { code: 'HT', name: 'Haiti' }, { code: 'HM', name: 'Heard Island and McDonald Islands' }, { code: 'HN', name: 'Honduras' }, { code: 'HK', name: 'Hong Kong SAR China' }, { code: 'HU', name: 'Hungary' }, { code: 'IS', name: 'Iceland' }, { code: 'IN', name: 'India' }, { code: 'ID', name: 'Indonesia' }, { code: 'IR', name: 'Iran' }, { code: 'IQ', name: 'Iraq' }, { code: 'IE', name: 'Ireland' }, { code: 'IM', name: 'Isle of Man' }, { code: 'IL', name: 'Israel' }, { code: 'IT', name: 'Italy' }, { code: 'JM', name: 'Jamaica' }, { code: 'JP', name: 'Japan' }, { code: 'JE', name: 'Jersey' }, { code: 'JO', name: 'Jordan' }, { code: 'KZ', name: 'Kazakhstan' }, { code: 'KE', name: 'Kenya' }, { code: 'KI', name: 'Kiribati' }, { code: 'KW', name: 'Kuwait' }, { code: 'KG', name: 'Kyrgyzstan' }, { code: 'LA', name: 'Laos' }, { code: 'LV', name: 'Latvia' }, { code: 'LB', name: 'Lebanon' }, { code: 'LS', name: 'Lesotho' }, { code: 'LR', name: 'Liberia' }, { code: 'LY', name: 'Libya' }, { code: 'LI', name: 'Liechtenstein' }, { code: 'LT', name: 'Lithuania' }, { code: 'LU', name: 'Luxembourg' }, { code: 'MO', name: 'Macau SAR China' }, { code: 'MK', name: 'Macedonia' }, { code: 'MG', name: 'Madagascar' }, { code: 'MW', name: 'Malawi' }, { code: 'MY', name: 'Malaysia' }, { code: 'MV', name: 'Maldives' }, { code: 'ML', name: 'Mali' }, { code: 'MT', name: 'Malta' }, { code: 'MH', name: 'Marshall Islands' }, { code: 'MQ', name: 'Martinique' }, { code: 'MR', name: 'Mauritania' }, { code: 'MU', name: 'Mauritius' }, { code: 'YT', name: 'Mayotte' }, { code: 'MX', name: 'Mexico' }, { code: 'FM', name: 'Micronesia' }, { code: 'MD', name: 'Moldova' }, { code: 'MC', name: 'Monaco' }, { code: 'MN', name: 'Mongolia' }, { code: 'ME', name: 'Montenegro' }, { code: 'MS', name: 'Montserrat' }, { code: 'MA', name: 'Morocco' }, { code: 'MZ', name: 'Mozambique' }, { code: 'MM', name: 'Myanmar [Burma]' }, { code: 'NA', name: 'Namibia' }, { code: 'NR', name: 'Nauru' }, { code: 'NP', name: 'Nepal' }, { code: 'NL', name: 'Netherlands' }, { code: 'AN', name: 'Netherlands Antilles' }, { code: 'NC', name: 'New Caledonia' }, { code: 'NZ', name: 'New Zealand' }, { code: 'NI', name: 'Nicaragua' }, { code: 'NE', name: 'Niger' }, { code: 'NG', name: 'Nigeria' }, { code: 'NU', name: 'Niue' }, { code: 'NF', name: 'Norfolk Island' }, { code: 'KP', name: 'North Korea' }, { code: 'MP', name: 'Northern Mariana Islands' }, { code: 'NO', name: 'Norway' }, { code: 'OM', name: 'Oman' }, { code: 'PK', name: 'Pakistan' }, { code: 'PW', name: 'Palau' }, { code: 'PS', name: 'Palestinian Territories' }, { code: 'PA', name: 'Panama' }, { code: 'PG', name: 'Papua New Guinea' }, { code: 'PY', name: 'Paraguay' }, { code: 'PE', name: 'Peru' }, { code: 'PH', name: 'Philippines' }, { code: 'PN', name: 'Pitcairn Islands' }, { code: 'PL', name: 'Poland' }, { code: 'PT', name: 'Portugal' }, { code: 'PR', name: 'Puerto Rico' }, { code: 'QA', name: 'Qatar' }, { code: 'RO', name: 'Romania' }, { code: 'RU', name: 'Russia' }, { code: 'RW', name: 'Rwanda' }, { code: 'RE', name: 'Réunion' }, { code: 'BL', name: 'Saint Barthélemy' }, { code: 'SH', name: 'Saint Helena' }, { code: 'KN', name: 'Saint Kitts and Nevis' }, { code: 'LC', name: 'Saint Lucia' }, { code: 'MF', name: 'Saint Martin' }, { code: 'PM', name: 'Saint Pierre and Miquelon' }, { code: 'VC', name: 'Saint Vincent and the Grenadines' }, { code: 'WS', name: 'Samoa' }, { code: 'SM', name: 'San Marino' }, { code: 'SA', name: 'Saudi Arabia' }, { code: 'SN', name: 'Senegal' }, { code: 'RS', name: 'Serbia' }, { code: 'SC', name: 'Seychelles' }, { code: 'SL', name: 'Sierra Leone' }, { code: 'SG', name: 'Singapore' }, { code: 'SK', name: 'Slovakia' }, { code: 'SI', name: 'Slovenia' }, { code: 'SB', name: 'Solomon Islands' }, { code: 'SO', name: 'Somalia' }, { code: 'ZA', name: 'South Africa' }, { code: 'GS', name: 'South Georgia and the South Sandwich Islands' }, { code: 'KR', name: 'South Korea' }, { code: 'ES', name: 'Spain' }, { code: 'LK', name: 'Sri Lanka' }, { code: 'SD', name: 'Sudan' }, { code: 'SR', name: 'Suriname' }, { code: 'SJ', name: 'Svalbard and Jan Mayen' }, { code: 'SZ', name: 'Swaziland' }, { code: 'SE', name: 'Sweden' }, { code: 'CH', name: 'Switzerland' }, { code: 'SY', name: 'Syria' }, { code: 'ST', name: 'São Tomé and Príncipe' }, { code: 'TW', name: 'Taiwan' }, { code: 'TJ', name: 'Tajikistan' }, { code: 'TZ', name: 'Tanzania' }, { code: 'TH', name: 'Thailand' }, { code: 'TL', name: 'Timor-Leste' }, { code: 'TG', name: 'Togo' }, { code: 'TK', name: 'Tokelau' }, { code: 'TO', name: 'Tonga' }, { code: 'TT', name: 'Trinidad and Tobago' }, { code: 'TN', name: 'Tunisia' }, { code: 'TR', name: 'Turkey' }, { code: 'TM', name: 'Turkmenistan' }, { code: 'TC', name: 'Turks and Caicos Islands' }, { code: 'TV', name: 'Tuvalu' }, { code: 'UM', name: 'U.S. Minor Outlying Islands' }, { code: 'VI', name: 'U.S. Virgin Islands' }, { code: 'UG', name: 'Uganda' }, { code: 'UA', name: 'Ukraine' }, { code: 'AE', name: 'United Arab Emirates' }, { code: 'GB', name: 'United Kingdom' }, { code: 'US', name: 'United States' }, { code: 'UY', name: 'Uruguay' }, { code: 'UZ', name: 'Uzbekistan' }, { code: 'VU', name: 'Vanuatu' }, { code: 'VA', name: 'Vatican City' }, { code: 'VE', name: 'Venezuela' }, { code: 'VN', name: 'Vietnam' }, { code: 'WF', name: 'Wallis and Futuna' }, { code: 'EH', name: 'Western Sahara' }, { code: 'YE', name: 'Yemen' }, { code: 'ZM', name: 'Zambia' }, { code: 'ZW', name: 'Zimbabwe' }, { code: 'AX', name: 'Åland Islands' } ]; ================================================ FILE: packages/evershop/src/lib/locale/currencies.ts ================================================ export interface Currency { code: string; name: string; } export const currencies: Currency[] = [ { code: 'AFN', name: 'Afghan Afghani' }, { code: 'ALL', name: 'Albanian Lek' }, { code: 'DZD', name: 'Algerian Dinar' }, { code: 'AOA', name: 'Angolan Kwanza' }, { code: 'ARS', name: 'Argentine Peso' }, { code: 'AMD', name: 'Armenian Dram' }, { code: 'AWG', name: 'Aruban Florin' }, { code: 'AUD', name: 'Australian Dollar' }, { code: 'AZN', name: 'Azerbaijani Manat' }, { code: 'AZM', name: 'Azerbaijani Manat (1993-2006)' }, { code: 'BSD', name: 'Bahamian Dollar' }, { code: 'BHD', name: 'Bahraini Dinar' }, { code: 'BDT', name: 'Bangladeshi Taka' }, { code: 'BBD', name: 'Barbadian Dollar' }, { code: 'BYR', name: 'Belarusian Ruble' }, { code: 'BZD', name: 'Belize Dollar' }, { code: 'BMD', name: 'Bermudan Dollar' }, { code: 'BTN', name: 'Bhutanese Ngultrum' }, { code: 'BOB', name: 'Bolivian Boliviano' }, { code: 'BAM', name: 'Bosnia-Herzegovina Convertible Mark' }, { code: 'BWP', name: 'Botswanan Pula' }, { code: 'BRL', name: 'Brazilian Real' }, { code: 'GBP', name: 'British Pound Sterling' }, { code: 'BND', name: 'Brunei Dollar' }, { code: 'BGN', name: 'Bulgarian Lev' }, { code: 'BUK', name: 'Burmese Kyat' }, { code: 'BIF', name: 'Burundian Franc' }, { code: 'XOF', name: 'CFA Franc BCEAO' }, { code: 'XPF', name: 'CFP Franc' }, { code: 'KHR', name: 'Cambodian Riel' }, { code: 'CAD', name: 'Canadian Dollar' }, { code: 'CVE', name: 'Cape Verdean Escudo' }, { code: 'KYD', name: 'Cayman Islands Dollar' }, { code: 'CLP', name: 'Chilean Peso' }, { code: 'CNY', name: 'Chinese Yuan Renminbi' }, { code: 'COP', name: 'Colombian Peso' }, { code: 'KMF', name: 'Comorian Franc' }, { code: 'CDF', name: 'Congolese Franc' }, { code: 'CRC', name: 'Costa Rican Colón' }, { code: 'HRK', name: 'Croatian Kuna' }, { code: 'CUP', name: 'Cuban Peso' }, { code: 'CZK', name: 'Czech Republic Koruna' }, { code: 'DKK', name: 'Danish Krone' }, { code: 'DJF', name: 'Djiboutian Franc' }, { code: 'DOP', name: 'Dominican Peso' }, { code: 'XCD', name: 'East Caribbean Dollar' }, { code: 'EGP', name: 'Egyptian Pound' }, { code: 'GQE', name: 'Equatorial Guinean Ekwele' }, { code: 'ERN', name: 'Eritrean Nakfa' }, { code: 'EEK', name: 'Estonian Kroon' }, { code: 'ETB', name: 'Ethiopian Birr' }, { code: 'EUR', name: 'Euro' }, { code: 'FKP', name: 'Falkland Islands Pound' }, { code: 'FJD', name: 'Fijian Dollar' }, { code: 'GMD', name: 'Gambian Dalasi' }, { code: 'GEK', name: 'Georgian Kupon Larit' }, { code: 'GEL', name: 'Georgian Lari' }, { code: 'GHS', name: 'Ghanaian Cedi' }, { code: 'GIP', name: 'Gibraltar Pound' }, { code: 'GTQ', name: 'Guatemalan Quetzal' }, { code: 'GNF', name: 'Guinean Franc' }, { code: 'GYD', name: 'Guyanaese Dollar' }, { code: 'HTG', name: 'Haitian Gourde' }, { code: 'HNL', name: 'Honduran Lempira' }, { code: 'HKD', name: 'Hong Kong Dollar' }, { code: 'HUF', name: 'Hungarian Forint' }, { code: 'ISK', name: 'Icelandic Króna' }, { code: 'INR', name: 'Indian Rupee' }, { code: 'IDR', name: 'Indonesian Rupiah' }, { code: 'IRR', name: 'Iranian Rial' }, { code: 'IQD', name: 'Iraqi Dinar' }, { code: 'ILS', name: 'Israeli New Sheqel' }, { code: 'JMD', name: 'Jamaican Dollar' }, { code: 'JPY', name: 'Japanese Yen' }, { code: 'JOD', name: 'Jordanian Dinar' }, { code: 'KZT', name: 'Kazakhstan Tenge' }, { code: 'KES', name: 'Kenyan Shilling' }, { code: 'KWD', name: 'Kuwaiti Dinar' }, { code: 'KGS', name: 'Kyrgystani Som' }, { code: 'LAK', name: 'Laotian Kip' }, { code: 'LVL', name: 'Latvian Lats' }, { code: 'LBP', name: 'Lebanese Pound' }, { code: 'LSL', name: 'Lesotho Loti' }, { code: 'LRD', name: 'Liberian Dollar' }, { code: 'LYD', name: 'Libyan Dinar' }, { code: 'LTL', name: 'Lithuanian Litas' }, { code: 'MOP', name: 'Macanese Pataca' }, { code: 'MKD', name: 'Macedonian Denar' }, { code: 'MGA', name: 'Malagasy Ariary' }, { code: 'MWK', name: 'Malawian Kwacha' }, { code: 'MYR', name: 'Malaysian Ringgit' }, { code: 'MVR', name: 'Maldivian Rufiyaa' }, { code: 'MRO', name: 'Mauritanian Ouguiya' }, { code: 'MUR', name: 'Mauritian Rupee' }, { code: 'MXN', name: 'Mexican Peso' }, { code: 'MDL', name: 'Moldovan Leu' }, { code: 'MNT', name: 'Mongolian Tugrik' }, { code: 'MAD', name: 'Moroccan Dirham' }, { code: 'MZN', name: 'Mozambican Metical' }, { code: 'MMK', name: 'Myanma Kyat' }, { code: 'NAD', name: 'Namibian Dollar' }, { code: 'NPR', name: 'Nepalese Rupee' }, { code: 'ANG', name: 'Netherlands Antillean Guilder' }, { code: 'TWD', name: 'New Taiwan Dollar' }, { code: 'NZD', name: 'New Zealand Dollar' }, { code: 'NIC', name: 'Nicaraguan Cordoba' }, { code: 'NGN', name: 'Nigerian Naira' }, { code: 'KPW', name: 'North Korean Won' }, { code: 'NOK', name: 'Norwegian Krone' }, { code: 'ROL', name: 'Old Romanian Leu' }, { code: 'TRL', name: 'Old Turkish Lira' }, { code: 'OMR', name: 'Omani Rial' }, { code: 'PKR', name: 'Pakistani Rupee' }, { code: 'PAB', name: 'Panamanian Balboa' }, { code: 'PGK', name: 'Papua New Guinean Kina' }, { code: 'PYG', name: 'Paraguayan Guarani' }, { code: 'PEN', name: 'Peruvian Nuevo Sol' }, { code: 'PHP', name: 'Philippine Peso' }, { code: 'PLN', name: 'Polish Zloty' }, { code: 'QAR', name: 'Qatari Rial' }, { code: 'RHD', name: 'Rhodesian Dollar' }, { code: 'RON', name: 'Romanian Leu' }, { code: 'RUB', name: 'Russian Ruble' }, { code: 'RWF', name: 'Rwandan Franc' }, { code: 'SHP', name: 'Saint Helena Pound' }, { code: 'SVC', name: 'Salvadoran Colón' }, { code: 'WST', name: 'Samoan Tala' }, { code: 'SAR', name: 'Saudi Riyal' }, { code: 'RSD', name: 'Serbian Dinar' }, { code: 'SCR', name: 'Seychellois Rupee' }, { code: 'SLL', name: 'Sierra Leonean Leone' }, { code: 'SGD', name: 'Singapore Dollar' }, { code: 'SKK', name: 'Slovak Koruna' }, { code: 'SBD', name: 'Solomon Islands Dollar' }, { code: 'SOS', name: 'Somali Shilling' }, { code: 'ZAR', name: 'South African Rand' }, { code: 'KRW', name: 'South Korean Won' }, { code: 'LKR', name: 'Sri Lanka Rupee' }, { code: 'SDG', name: 'Sudanese Pound' }, { code: 'SRD', name: 'Surinamese Dollar' }, { code: 'SZL', name: 'Swazi Lilangeni' }, { code: 'SEK', name: 'Swedish Krona' }, { code: 'CHF', name: 'Swiss Franc' }, { code: 'SYP', name: 'Syrian Pound' }, { code: 'STD', name: 'São Tomé and Príncipe Dobra' }, { code: 'TJS', name: 'Tajikistani Somoni' }, { code: 'TZS', name: 'Tanzanian Shilling' }, { code: 'THB', name: 'Thai Baht' }, { code: 'TOP', name: 'Tongan Paʻanga' }, { code: 'TTD', name: 'Trinidad and Tobago Dollar' }, { code: 'TND', name: 'Tunisian Dinar' }, { code: 'TRY', name: 'Turkish Lira' }, { code: 'TMM', name: 'Turkmenistani Manat' }, { code: 'USD', name: 'US Dollar' }, { code: 'UGX', name: 'Ugandan Shilling' }, { code: 'UAH', name: 'Ukrainian Hryvnia' }, { code: 'AED', name: 'United Arab Emirates Dirham' }, { code: 'UYU', name: 'Uruguayan Peso' }, { code: 'UZS', name: 'Uzbekistan Som' }, { code: 'VUV', name: 'Vanuatu Vatu' }, { code: 'VEB', name: 'Venezuelan Bolívar' }, { code: 'VEF', name: 'Venezuelan Bolívar Fuerte' }, { code: 'VND', name: 'Vietnamese Dong' }, { code: 'CHE', name: 'WIR Euro' }, { code: 'CHW', name: 'WIR Franc' }, { code: 'YER', name: 'Yemeni Rial' }, { code: 'ZMK', name: 'Zambian Kwacha' }, { code: 'ZWD', name: 'Zimbabwean Dollar' } ]; ================================================ FILE: packages/evershop/src/lib/locale/index.ts ================================================ export * from './countries.js'; export * from './currencies.js'; export * from './provinces.js'; export * from './timezones.js'; ================================================ FILE: packages/evershop/src/lib/locale/provinces.ts ================================================ export interface Province { code: string; countryCode: string; name: string; } export const provinces: Province[] = [ { code: 'AD-07', countryCode: 'AD', name: 'Andorra la Vella' }, { code: 'AD-02', countryCode: 'AD', name: 'Canillo' }, { code: 'AD-03', countryCode: 'AD', name: 'Encamp' }, { code: 'AD-08', countryCode: 'AD', name: 'Escaldes-Engordany' }, { code: 'AD-04', countryCode: 'AD', name: 'La Massana' }, { code: 'AD-05', countryCode: 'AD', name: 'Ordino' }, { code: 'AD-06', countryCode: 'AD', name: 'Sant Julia de Loria' }, { code: 'AE-AJ', countryCode: 'AE', name: "'Ajman" }, { code: 'AE-AZ', countryCode: 'AE', name: 'Abu Zaby' }, { code: 'AE-FU', countryCode: 'AE', name: 'Al Fujayrah' }, { code: 'AE-SH', countryCode: 'AE', name: 'Ash Shariqah' }, { code: 'AE-DU', countryCode: 'AE', name: 'Dubayy' }, { code: 'AE-RK', countryCode: 'AE', name: "Ra's al Khaymah" }, { code: 'AE-UQ', countryCode: 'AE', name: 'Umm al Qaywayn' }, { code: 'AF-BDS', countryCode: 'AF', name: 'Badakhshan' }, { code: 'AF-BDG', countryCode: 'AF', name: 'Badghis' }, { code: 'AF-BGL', countryCode: 'AF', name: 'Baghlan' }, { code: 'AF-BAL', countryCode: 'AF', name: 'Balkh' }, { code: 'AF-BAM', countryCode: 'AF', name: 'Bamyan' }, { code: 'AF-DAY', countryCode: 'AF', name: 'Daykundi' }, { code: 'AF-FRA', countryCode: 'AF', name: 'Farah' }, { code: 'AF-FYB', countryCode: 'AF', name: 'Faryab' }, { code: 'AF-GHA', countryCode: 'AF', name: 'Ghazni' }, { code: 'AF-GHO', countryCode: 'AF', name: 'Ghor' }, { code: 'AF-HEL', countryCode: 'AF', name: 'Helmand' }, { code: 'AF-HER', countryCode: 'AF', name: 'Herat' }, { code: 'AF-JOW', countryCode: 'AF', name: 'Jowzjan' }, { code: 'AF-KAB', countryCode: 'AF', name: 'Kabul' }, { code: 'AF-KAN', countryCode: 'AF', name: 'Kandahar' }, { code: 'AF-KAP', countryCode: 'AF', name: 'Kapisa' }, { code: 'AF-KHO', countryCode: 'AF', name: 'Khost' }, { code: 'AF-KNR', countryCode: 'AF', name: 'Kunar' }, { code: 'AF-KDZ', countryCode: 'AF', name: 'Kunduz' }, { code: 'AF-LAG', countryCode: 'AF', name: 'Laghman' }, { code: 'AF-LOG', countryCode: 'AF', name: 'Logar' }, { code: 'AF-NAN', countryCode: 'AF', name: 'Nangarhar' }, { code: 'AF-NIM', countryCode: 'AF', name: 'Nimroz' }, { code: 'AF-NUR', countryCode: 'AF', name: 'Nuristan' }, { code: 'AF-PKA', countryCode: 'AF', name: 'Paktika' }, { code: 'AF-PIA', countryCode: 'AF', name: 'Paktiya' }, { code: 'AF-PAN', countryCode: 'AF', name: 'Panjshayr' }, { code: 'AF-PAR', countryCode: 'AF', name: 'Parwan' }, { code: 'AF-SAM', countryCode: 'AF', name: 'Samangan' }, { code: 'AF-SAR', countryCode: 'AF', name: 'Sar-e Pul' }, { code: 'AF-TAK', countryCode: 'AF', name: 'Takhar' }, { code: 'AF-URU', countryCode: 'AF', name: 'Uruzgan' }, { code: 'AF-WAR', countryCode: 'AF', name: 'Wardak' }, { code: 'AF-ZAB', countryCode: 'AF', name: 'Zabul' }, { code: 'AG-04', countryCode: 'AG', name: 'Saint John' }, { code: 'AG-05', countryCode: 'AG', name: 'Saint Mary' }, { code: 'AG-06', countryCode: 'AG', name: 'Saint Paul' }, { code: 'AL-01', countryCode: 'AL', name: 'Berat' }, { code: 'AL-09', countryCode: 'AL', name: 'Diber' }, { code: 'AL-02', countryCode: 'AL', name: 'Durres' }, { code: 'AL-03', countryCode: 'AL', name: 'Elbasan' }, { code: 'AL-04', countryCode: 'AL', name: 'Fier' }, { code: 'AL-05', countryCode: 'AL', name: 'Gjirokaster' }, { code: 'AL-06', countryCode: 'AL', name: 'Korce' }, { code: 'AL-07', countryCode: 'AL', name: 'Kukes' }, { code: 'AL-08', countryCode: 'AL', name: 'Lezhe' }, { code: 'AL-10', countryCode: 'AL', name: 'Shkoder' }, { code: 'AL-11', countryCode: 'AL', name: 'Tirane' }, { code: 'AL-12', countryCode: 'AL', name: 'Vlore' }, { code: 'AM-AG', countryCode: 'AM', name: 'Aragacotn' }, { code: 'AM-AR', countryCode: 'AM', name: 'Ararat' }, { code: 'AM-AV', countryCode: 'AM', name: 'Armavir' }, { code: 'AM-ER', countryCode: 'AM', name: 'Erevan' }, { code: 'AM-GR', countryCode: 'AM', name: "Gegark'unik'" }, { code: 'AM-KT', countryCode: 'AM', name: "Kotayk'" }, { code: 'AM-LO', countryCode: 'AM', name: 'Lori' }, { code: 'AM-SH', countryCode: 'AM', name: 'Sirak' }, { code: 'AM-SU', countryCode: 'AM', name: "Syunik'" }, { code: 'AM-TV', countryCode: 'AM', name: 'Tavus' }, { code: 'AM-VD', countryCode: 'AM', name: 'Vayoc Jor' }, { code: 'AO-BGO', countryCode: 'AO', name: 'Bengo' }, { code: 'AO-BGU', countryCode: 'AO', name: 'Benguela' }, { code: 'AO-BIE', countryCode: 'AO', name: 'Bie' }, { code: 'AO-CAB', countryCode: 'AO', name: 'Cabinda' }, { code: 'AO-CNN', countryCode: 'AO', name: 'Cunene' }, { code: 'AO-HUA', countryCode: 'AO', name: 'Huambo' }, { code: 'AO-HUI', countryCode: 'AO', name: 'Huila' }, { code: 'AO-CCU', countryCode: 'AO', name: 'Kuando Kubango' }, { code: 'AO-CNO', countryCode: 'AO', name: 'Kwanza Norte' }, { code: 'AO-CUS', countryCode: 'AO', name: 'Kwanza Sul' }, { code: 'AO-LUA', countryCode: 'AO', name: 'Luanda' }, { code: 'AO-LNO', countryCode: 'AO', name: 'Lunda Norte' }, { code: 'AO-LSU', countryCode: 'AO', name: 'Lunda Sul' }, { code: 'AO-MAL', countryCode: 'AO', name: 'Malange' }, { code: 'AO-MOX', countryCode: 'AO', name: 'Moxico' }, { code: 'AO-NAM', countryCode: 'AO', name: 'Namibe' }, { code: 'AO-UIG', countryCode: 'AO', name: 'Uige' }, { code: 'AO-ZAI', countryCode: 'AO', name: 'Zaire' }, { code: 'AR-B', countryCode: 'AR', name: 'Buenos Aires' }, { code: 'AR-K', countryCode: 'AR', name: 'Catamarca' }, { code: 'AR-H', countryCode: 'AR', name: 'Chaco' }, { code: 'AR-U', countryCode: 'AR', name: 'Chubut' }, { code: 'AR-C', countryCode: 'AR', name: 'Ciudad Autonoma de Buenos Aires' }, { code: 'AR-X', countryCode: 'AR', name: 'Cordoba' }, { code: 'AR-W', countryCode: 'AR', name: 'Corrientes' }, { code: 'AR-E', countryCode: 'AR', name: 'Entre Rios' }, { code: 'AR-P', countryCode: 'AR', name: 'Formosa' }, { code: 'AR-Y', countryCode: 'AR', name: 'Jujuy' }, { code: 'AR-L', countryCode: 'AR', name: 'La Pampa' }, { code: 'AR-F', countryCode: 'AR', name: 'La Rioja' }, { code: 'AR-M', countryCode: 'AR', name: 'Mendoza' }, { code: 'AR-N', countryCode: 'AR', name: 'Misiones' }, { code: 'AR-Q', countryCode: 'AR', name: 'Neuquen' }, { code: 'AR-R', countryCode: 'AR', name: 'Rio Negro' }, { code: 'AR-A', countryCode: 'AR', name: 'Salta' }, { code: 'AR-J', countryCode: 'AR', name: 'San Juan' }, { code: 'AR-D', countryCode: 'AR', name: 'San Luis' }, { code: 'AR-Z', countryCode: 'AR', name: 'Santa Cruz' }, { code: 'AR-S', countryCode: 'AR', name: 'Santa Fe' }, { code: 'AR-G', countryCode: 'AR', name: 'Santiago del Estero' }, { code: 'AR-V', countryCode: 'AR', name: 'Tierra del Fuego' }, { code: 'AR-T', countryCode: 'AR', name: 'Tucuman' }, { code: 'AT-1', countryCode: 'AT', name: 'Burgenland' }, { code: 'AT-2', countryCode: 'AT', name: 'Karnten' }, { code: 'AT-3', countryCode: 'AT', name: 'Niederosterreich' }, { code: 'AT-4', countryCode: 'AT', name: 'Oberosterreich' }, { code: 'AT-5', countryCode: 'AT', name: 'Salzburg' }, { code: 'AT-6', countryCode: 'AT', name: 'Steiermark' }, { code: 'AT-7', countryCode: 'AT', name: 'Tirol' }, { code: 'AT-8', countryCode: 'AT', name: 'Vorarlberg' }, { code: 'AT-9', countryCode: 'AT', name: 'Wien' }, { code: 'AU-ACT', countryCode: 'AU', name: 'Australian Capital Territory' }, { code: 'AU-NSW', countryCode: 'AU', name: 'New South Wales' }, { code: 'AU-NT', countryCode: 'AU', name: 'Northern Territory' }, { code: 'AU-QLD', countryCode: 'AU', name: 'Queensland' }, { code: 'AU-SA', countryCode: 'AU', name: 'South Australia' }, { code: 'AU-TAS', countryCode: 'AU', name: 'Tasmania' }, { code: 'AU-VIC', countryCode: 'AU', name: 'Victoria' }, { code: 'AU-WA', countryCode: 'AU', name: 'Western Australia' }, { code: 'AZ-ABS', countryCode: 'AZ', name: 'Abseron' }, { code: 'AZ-AGC', countryCode: 'AZ', name: 'Agcabadi' }, { code: 'AZ-AGM', countryCode: 'AZ', name: 'Agdam' }, { code: 'AZ-AGS', countryCode: 'AZ', name: 'Agdas' }, { code: 'AZ-AGA', countryCode: 'AZ', name: 'Agstafa' }, { code: 'AZ-AGU', countryCode: 'AZ', name: 'Agsu' }, { code: 'AZ-AST', countryCode: 'AZ', name: 'Astara' }, { code: 'AZ-BA', countryCode: 'AZ', name: 'Baki' }, { code: 'AZ-BAL', countryCode: 'AZ', name: 'Balakan' }, { code: 'AZ-BAR', countryCode: 'AZ', name: 'Barda' }, { code: 'AZ-BEY', countryCode: 'AZ', name: 'Beylaqan' }, { code: 'AZ-BIL', countryCode: 'AZ', name: 'Bilasuvar' }, { code: 'AZ-CAB', countryCode: 'AZ', name: 'Cabrayil' }, { code: 'AZ-CAL', countryCode: 'AZ', name: 'Calilabad' }, { code: 'AZ-DAS', countryCode: 'AZ', name: 'Daskasan' }, { code: 'AZ-FUZ', countryCode: 'AZ', name: 'Fuzuli' }, { code: 'AZ-GAD', countryCode: 'AZ', name: 'Gadabay' }, { code: 'AZ-GA', countryCode: 'AZ', name: 'Ganca' }, { code: 'AZ-GOR', countryCode: 'AZ', name: 'Goranboy' }, { code: 'AZ-GOY', countryCode: 'AZ', name: 'Goycay' }, { code: 'AZ-GYG', countryCode: 'AZ', name: 'Goygol' }, { code: 'AZ-HAC', countryCode: 'AZ', name: 'Haciqabul' }, { code: 'AZ-IMI', countryCode: 'AZ', name: 'Imisli' }, { code: 'AZ-ISM', countryCode: 'AZ', name: 'Ismayilli' }, { code: 'AZ-KAL', countryCode: 'AZ', name: 'Kalbacar' }, { code: 'AZ-LAC', countryCode: 'AZ', name: 'Lacin' }, { code: 'AZ-LA', countryCode: 'AZ', name: 'Lankaran' }, { code: 'AZ-LER', countryCode: 'AZ', name: 'Lerik' }, { code: 'AZ-MAS', countryCode: 'AZ', name: 'Masalli' }, { code: 'AZ-MI', countryCode: 'AZ', name: 'Mingacevir' }, { code: 'AZ-NA', countryCode: 'AZ', name: 'Naftalan' }, { code: 'AZ-NX', countryCode: 'AZ', name: 'Naxcivan' }, { code: 'AZ-NEF', countryCode: 'AZ', name: 'Neftcala' }, { code: 'AZ-OGU', countryCode: 'AZ', name: 'Oguz' }, { code: 'AZ-QAB', countryCode: 'AZ', name: 'Qabala' }, { code: 'AZ-QAX', countryCode: 'AZ', name: 'Qax' }, { code: 'AZ-QAZ', countryCode: 'AZ', name: 'Qazax' }, { code: 'AZ-QOB', countryCode: 'AZ', name: 'Qobustan' }, { code: 'AZ-QBA', countryCode: 'AZ', name: 'Quba' }, { code: 'AZ-QBI', countryCode: 'AZ', name: 'Qubadli' }, { code: 'AZ-QUS', countryCode: 'AZ', name: 'Qusar' }, { code: 'AZ-SAT', countryCode: 'AZ', name: 'Saatli' }, { code: 'AZ-SAB', countryCode: 'AZ', name: 'Sabirabad' }, { code: 'AZ-SA', countryCode: 'AZ', name: 'Saki' }, { code: 'AZ-SAL', countryCode: 'AZ', name: 'Salyan' }, { code: 'AZ-SMI', countryCode: 'AZ', name: 'Samaxi' }, { code: 'AZ-SKR', countryCode: 'AZ', name: 'Samkir' }, { code: 'AZ-SMX', countryCode: 'AZ', name: 'Samux' }, { code: 'AZ-SR', countryCode: 'AZ', name: 'Sirvan' }, { code: 'AZ-SM', countryCode: 'AZ', name: 'Sumqayit' }, { code: 'AZ-SUS', countryCode: 'AZ', name: 'Susa' }, { code: 'AZ-TAR', countryCode: 'AZ', name: 'Tartar' }, { code: 'AZ-TOV', countryCode: 'AZ', name: 'Tovuz' }, { code: 'AZ-UCA', countryCode: 'AZ', name: 'Ucar' }, { code: 'AZ-XAC', countryCode: 'AZ', name: 'Xacmaz' }, { code: 'AZ-XA', countryCode: 'AZ', name: 'Xankandi' }, { code: 'AZ-XIZ', countryCode: 'AZ', name: 'Xizi' }, { code: 'AZ-XCI', countryCode: 'AZ', name: 'Xocali' }, { code: 'AZ-XVD', countryCode: 'AZ', name: 'Xocavand' }, { code: 'AZ-YAR', countryCode: 'AZ', name: 'Yardimli' }, { code: 'AZ-YE', countryCode: 'AZ', name: 'Yevlax' }, { code: 'AZ-ZAN', countryCode: 'AZ', name: 'Zangilan' }, { code: 'AZ-ZAQ', countryCode: 'AZ', name: 'Zaqatala' }, { code: 'AZ-ZAR', countryCode: 'AZ', name: 'Zardab' }, { code: 'BA-BIH', countryCode: 'BA', name: 'Federacija Bosne i Hercegovine' }, { code: 'BA-SRP', countryCode: 'BA', name: 'Republika Srpska' }, { code: 'BB-01', countryCode: 'BB', name: 'Christ Church' }, { code: 'BB-04', countryCode: 'BB', name: 'Saint James' }, { code: 'BB-06', countryCode: 'BB', name: 'Saint Joseph' }, { code: 'BB-08', countryCode: 'BB', name: 'Saint Michael' }, { code: 'BB-09', countryCode: 'BB', name: 'Saint Peter' }, { code: 'BD-06', countryCode: 'BD', name: 'Barisal' }, { code: 'BD-10', countryCode: 'BD', name: 'Chittagong' }, { code: 'BD-13', countryCode: 'BD', name: 'Dhaka' }, { code: 'BD-27', countryCode: 'BD', name: 'Khulna' }, { code: 'BD-54', countryCode: 'BD', name: 'Rajshahi' }, { code: 'BD-55', countryCode: 'BD', name: 'Rangpur' }, { code: 'BD-60', countryCode: 'BD', name: 'Sylhet' }, { code: 'BE-VAN', countryCode: 'BE', name: 'Antwerpen' }, { code: 'BE-WBR', countryCode: 'BE', name: 'Brabant wallon' }, { code: 'BE-BRU', countryCode: 'BE', name: 'Brussels Hoofdstedelijk Gewest' }, { code: 'BE-WHT', countryCode: 'BE', name: 'Hainaut' }, { code: 'BE-WLG', countryCode: 'BE', name: 'Liege' }, { code: 'BE-VLI', countryCode: 'BE', name: 'Limburg' }, { code: 'BE-WLX', countryCode: 'BE', name: 'Luxembourg' }, { code: 'BE-WNA', countryCode: 'BE', name: 'Namur' }, { code: 'BE-VOV', countryCode: 'BE', name: 'Oost-Vlaanderen' }, { code: 'BE-VBR', countryCode: 'BE', name: 'Vlaams-Brabant' }, { code: 'BE-VWV', countryCode: 'BE', name: 'West-Vlaanderen' }, { code: 'BF-BAL', countryCode: 'BF', name: 'Bale' }, { code: 'BF-BAM', countryCode: 'BF', name: 'Bam' }, { code: 'BF-BAN', countryCode: 'BF', name: 'Banwa' }, { code: 'BF-BAZ', countryCode: 'BF', name: 'Bazega' }, { code: 'BF-BGR', countryCode: 'BF', name: 'Bougouriba' }, { code: 'BF-BLG', countryCode: 'BF', name: 'Boulgou' }, { code: 'BF-BLK', countryCode: 'BF', name: 'Boulkiemde' }, { code: 'BF-COM', countryCode: 'BF', name: 'Comoe' }, { code: 'BF-GAN', countryCode: 'BF', name: 'Ganzourgou' }, { code: 'BF-GNA', countryCode: 'BF', name: 'Gnagna' }, { code: 'BF-GOU', countryCode: 'BF', name: 'Gourma' }, { code: 'BF-HOU', countryCode: 'BF', name: 'Houet' }, { code: 'BF-IOB', countryCode: 'BF', name: 'Ioba' }, { code: 'BF-KAD', countryCode: 'BF', name: 'Kadiogo' }, { code: 'BF-KEN', countryCode: 'BF', name: 'Kenedougou' }, { code: 'BF-KMD', countryCode: 'BF', name: 'Komondjari' }, { code: 'BF-KMP', countryCode: 'BF', name: 'Kompienga' }, { code: 'BF-KOS', countryCode: 'BF', name: 'Kossi' }, { code: 'BF-KOP', countryCode: 'BF', name: 'Koulpelogo' }, { code: 'BF-KOT', countryCode: 'BF', name: 'Kouritenga' }, { code: 'BF-KOW', countryCode: 'BF', name: 'Kourweogo' }, { code: 'BF-LER', countryCode: 'BF', name: 'Leraba' }, { code: 'BF-LOR', countryCode: 'BF', name: 'Loroum' }, { code: 'BF-MOU', countryCode: 'BF', name: 'Mouhoun' }, { code: 'BF-NAO', countryCode: 'BF', name: 'Nahouri' }, { code: 'BF-NAM', countryCode: 'BF', name: 'Namentenga' }, { code: 'BF-NAY', countryCode: 'BF', name: 'Nayala' }, { code: 'BF-NOU', countryCode: 'BF', name: 'Noumbiel' }, { code: 'BF-OUB', countryCode: 'BF', name: 'Oubritenga' }, { code: 'BF-OUD', countryCode: 'BF', name: 'Oudalan' }, { code: 'BF-PAS', countryCode: 'BF', name: 'Passore' }, { code: 'BF-PON', countryCode: 'BF', name: 'Poni' }, { code: 'BF-SNG', countryCode: 'BF', name: 'Sanguie' }, { code: 'BF-SMT', countryCode: 'BF', name: 'Sanmatenga' }, { code: 'BF-SEN', countryCode: 'BF', name: 'Seno' }, { code: 'BF-SIS', countryCode: 'BF', name: 'Sissili' }, { code: 'BF-SOM', countryCode: 'BF', name: 'Soum' }, { code: 'BF-SOR', countryCode: 'BF', name: 'Sourou' }, { code: 'BF-TAP', countryCode: 'BF', name: 'Tapoa' }, { code: 'BF-TUI', countryCode: 'BF', name: 'Tuy' }, { code: 'BF-YAG', countryCode: 'BF', name: 'Yagha' }, { code: 'BF-YAT', countryCode: 'BF', name: 'Yatenga' }, { code: 'BF-ZIR', countryCode: 'BF', name: 'Ziro' }, { code: 'BF-ZON', countryCode: 'BF', name: 'Zondoma' }, { code: 'BF-ZOU', countryCode: 'BF', name: 'Zoundweogo' }, { code: 'BG-01', countryCode: 'BG', name: 'Blagoevgrad' }, { code: 'BG-02', countryCode: 'BG', name: 'Burgas' }, { code: 'BG-08', countryCode: 'BG', name: 'Dobrich' }, { code: 'BG-07', countryCode: 'BG', name: 'Gabrovo' }, { code: 'BG-26', countryCode: 'BG', name: 'Haskovo' }, { code: 'BG-09', countryCode: 'BG', name: 'Kardzhali' }, { code: 'BG-10', countryCode: 'BG', name: 'Kyustendil' }, { code: 'BG-11', countryCode: 'BG', name: 'Lovech' }, { code: 'BG-12', countryCode: 'BG', name: 'Montana' }, { code: 'BG-13', countryCode: 'BG', name: 'Pazardzhik' }, { code: 'BG-14', countryCode: 'BG', name: 'Pernik' }, { code: 'BG-15', countryCode: 'BG', name: 'Pleven' }, { code: 'BG-16', countryCode: 'BG', name: 'Plovdiv' }, { code: 'BG-17', countryCode: 'BG', name: 'Razgrad' }, { code: 'BG-18', countryCode: 'BG', name: 'Ruse' }, { code: 'BG-27', countryCode: 'BG', name: 'Shumen' }, { code: 'BG-19', countryCode: 'BG', name: 'Silistra' }, { code: 'BG-20', countryCode: 'BG', name: 'Sliven' }, { code: 'BG-21', countryCode: 'BG', name: 'Smolyan' }, { code: 'BG-23', countryCode: 'BG', name: 'Sofia' }, { code: 'BG-22', countryCode: 'BG', name: 'Sofia (stolitsa)' }, { code: 'BG-24', countryCode: 'BG', name: 'Stara Zagora' }, { code: 'BG-25', countryCode: 'BG', name: 'Targovishte' }, { code: 'BG-03', countryCode: 'BG', name: 'Varna' }, { code: 'BG-04', countryCode: 'BG', name: 'Veliko Tarnovo' }, { code: 'BG-05', countryCode: 'BG', name: 'Vidin' }, { code: 'BG-06', countryCode: 'BG', name: 'Vratsa' }, { code: 'BG-28', countryCode: 'BG', name: 'Yambol' }, { code: 'BH-13', countryCode: 'BH', name: "Al 'Asimah" }, { code: 'BH-15', countryCode: 'BH', name: 'Al Muharraq' }, { code: 'BH-17', countryCode: 'BH', name: 'Ash Shamaliyah' }, { code: 'BI-BB', countryCode: 'BI', name: 'Bubanza' }, { code: 'BI-BM', countryCode: 'BI', name: 'Bujumbura Mairie' }, { code: 'BI-BR', countryCode: 'BI', name: 'Bururi' }, { code: 'BI-CA', countryCode: 'BI', name: 'Cankuzo' }, { code: 'BI-CI', countryCode: 'BI', name: 'Cibitoke' }, { code: 'BI-GI', countryCode: 'BI', name: 'Gitega' }, { code: 'BI-KR', countryCode: 'BI', name: 'Karuzi' }, { code: 'BI-KY', countryCode: 'BI', name: 'Kayanza' }, { code: 'BI-KI', countryCode: 'BI', name: 'Kirundo' }, { code: 'BI-MA', countryCode: 'BI', name: 'Makamba' }, { code: 'BI-MU', countryCode: 'BI', name: 'Muramvya' }, { code: 'BI-MY', countryCode: 'BI', name: 'Muyinga' }, { code: 'BI-MW', countryCode: 'BI', name: 'Mwaro' }, { code: 'BI-NG', countryCode: 'BI', name: 'Ngozi' }, { code: 'BI-RT', countryCode: 'BI', name: 'Rutana' }, { code: 'BI-RY', countryCode: 'BI', name: 'Ruyigi' }, { code: 'BJ-AL', countryCode: 'BJ', name: 'Alibori' }, { code: 'BJ-AK', countryCode: 'BJ', name: 'Atacora' }, { code: 'BJ-AQ', countryCode: 'BJ', name: 'Atlantique' }, { code: 'BJ-BO', countryCode: 'BJ', name: 'Borgou' }, { code: 'BJ-CO', countryCode: 'BJ', name: 'Collines' }, { code: 'BJ-KO', countryCode: 'BJ', name: 'Couffo' }, { code: 'BJ-DO', countryCode: 'BJ', name: 'Donga' }, { code: 'BJ-LI', countryCode: 'BJ', name: 'Littoral' }, { code: 'BJ-MO', countryCode: 'BJ', name: 'Mono' }, { code: 'BJ-OU', countryCode: 'BJ', name: 'Oueme' }, { code: 'BJ-PL', countryCode: 'BJ', name: 'Plateau' }, { code: 'BJ-ZO', countryCode: 'BJ', name: 'Zou' }, { code: 'BN-BE', countryCode: 'BN', name: 'Belait' }, { code: 'BN-BM', countryCode: 'BN', name: 'Brunei-Muara' }, { code: 'BN-TE', countryCode: 'BN', name: 'Temburong' }, { code: 'BN-TU', countryCode: 'BN', name: 'Tutong' }, { code: 'BO-H', countryCode: 'BO', name: 'Chuquisaca' }, { code: 'BO-C', countryCode: 'BO', name: 'Cochabamba' }, { code: 'BO-B', countryCode: 'BO', name: 'El Beni' }, { code: 'BO-L', countryCode: 'BO', name: 'La Paz' }, { code: 'BO-O', countryCode: 'BO', name: 'Oruro' }, { code: 'BO-N', countryCode: 'BO', name: 'Pando' }, { code: 'BO-P', countryCode: 'BO', name: 'Potosi' }, { code: 'BO-S', countryCode: 'BO', name: 'Santa Cruz' }, { code: 'BO-T', countryCode: 'BO', name: 'Tarija' }, { code: 'BQ-BO', countryCode: 'BQ', name: 'Bonaire' }, { code: 'BQ-SA', countryCode: 'BQ', name: 'Saba' }, { code: 'BQ-SE', countryCode: 'BQ', name: 'Sint Eustatius' }, { code: 'BR-AC', countryCode: 'BR', name: 'Acre' }, { code: 'BR-AL', countryCode: 'BR', name: 'Alagoas' }, { code: 'BR-AP', countryCode: 'BR', name: 'Amapa' }, { code: 'BR-AM', countryCode: 'BR', name: 'Amazonas' }, { code: 'BR-BA', countryCode: 'BR', name: 'Bahia' }, { code: 'BR-CE', countryCode: 'BR', name: 'Ceara' }, { code: 'BR-DF', countryCode: 'BR', name: 'Distrito Federal' }, { code: 'BR-ES', countryCode: 'BR', name: 'Espirito Santo' }, { code: 'BR-GO', countryCode: 'BR', name: 'Goias' }, { code: 'BR-MA', countryCode: 'BR', name: 'Maranhao' }, { code: 'BR-MT', countryCode: 'BR', name: 'Mato Grosso' }, { code: 'BR-MS', countryCode: 'BR', name: 'Mato Grosso do Sul' }, { code: 'BR-MG', countryCode: 'BR', name: 'Minas Gerais' }, { code: 'BR-PA', countryCode: 'BR', name: 'Para' }, { code: 'BR-PB', countryCode: 'BR', name: 'Paraiba' }, { code: 'BR-PR', countryCode: 'BR', name: 'Parana' }, { code: 'BR-PE', countryCode: 'BR', name: 'Pernambuco' }, { code: 'BR-PI', countryCode: 'BR', name: 'Piaui' }, { code: 'BR-RJ', countryCode: 'BR', name: 'Rio de Janeiro' }, { code: 'BR-RN', countryCode: 'BR', name: 'Rio Grande do Norte' }, { code: 'BR-RS', countryCode: 'BR', name: 'Rio Grande do Sul' }, { code: 'BR-RO', countryCode: 'BR', name: 'Rondonia' }, { code: 'BR-RR', countryCode: 'BR', name: 'Roraima' }, { code: 'BR-SC', countryCode: 'BR', name: 'Santa Catarina' }, { code: 'BR-SP', countryCode: 'BR', name: 'Sao Paulo' }, { code: 'BR-SE', countryCode: 'BR', name: 'Sergipe' }, { code: 'BR-TO', countryCode: 'BR', name: 'Tocantins' }, { code: 'BS-CS', countryCode: 'BS', name: 'Central Andros' }, { code: 'BS-FP', countryCode: 'BS', name: 'City of Freeport' }, { code: 'BS-EG', countryCode: 'BS', name: 'East Grand Bahama' }, { code: 'BS-HI', countryCode: 'BS', name: 'Harbour Island' }, { code: 'BS-HT', countryCode: 'BS', name: 'Hope Town' }, { code: 'BS-LI', countryCode: 'BS', name: 'Long Island' }, { code: 'BS-SE', countryCode: 'BS', name: 'South Eleuthera' }, { code: 'BT-12', countryCode: 'BT', name: 'Chhukha' }, { code: 'BT-22', countryCode: 'BT', name: 'Dagana' }, { code: 'BT-GA', countryCode: 'BT', name: 'Gasa' }, { code: 'BT-13', countryCode: 'BT', name: 'Haa' }, { code: 'BT-42', countryCode: 'BT', name: 'Monggar' }, { code: 'BT-11', countryCode: 'BT', name: 'Paro' }, { code: 'BT-23', countryCode: 'BT', name: 'Punakha' }, { code: 'BT-15', countryCode: 'BT', name: 'Thimphu' }, { code: 'BT-TY', countryCode: 'BT', name: 'Trashi Yangtse' }, { code: 'BT-32', countryCode: 'BT', name: 'Trongsa' }, { code: 'BT-34', countryCode: 'BT', name: 'Zhemgang' }, { code: 'BW-CE', countryCode: 'BW', name: 'Central' }, { code: 'BW-GH', countryCode: 'BW', name: 'Ghanzi' }, { code: 'BW-KG', countryCode: 'BW', name: 'Kgalagadi' }, { code: 'BW-KL', countryCode: 'BW', name: 'Kgatleng' }, { code: 'BW-KW', countryCode: 'BW', name: 'Kweneng' }, { code: 'BW-NE', countryCode: 'BW', name: 'North East' }, { code: 'BW-NW', countryCode: 'BW', name: 'North West' }, { code: 'BW-SE', countryCode: 'BW', name: 'South East' }, { code: 'BW-SO', countryCode: 'BW', name: 'Southern' }, { code: 'BY-BR', countryCode: 'BY', name: "Brestskaya voblasts'" }, { code: 'BY-HO', countryCode: 'BY', name: "Homyel'skaya voblasts'" }, { code: 'BY-HR', countryCode: 'BY', name: "Hrodzenskaya voblasts'" }, { code: 'BY-MA', countryCode: 'BY', name: "Mahilyowskaya voblasts'" }, { code: 'BY-MI', countryCode: 'BY', name: "Minskaya voblasts'" }, { code: 'BY-VI', countryCode: 'BY', name: "Vitsyebskaya voblasts'" }, { code: 'BZ-BZ', countryCode: 'BZ', name: 'Belize' }, { code: 'BZ-CY', countryCode: 'BZ', name: 'Cayo' }, { code: 'BZ-CZL', countryCode: 'BZ', name: 'Corozal' }, { code: 'BZ-OW', countryCode: 'BZ', name: 'Orange Walk' }, { code: 'BZ-SC', countryCode: 'BZ', name: 'Stann Creek' }, { code: 'BZ-TOL', countryCode: 'BZ', name: 'Toledo' }, { code: 'CA-AB', countryCode: 'CA', name: 'Alberta' }, { code: 'CA-BC', countryCode: 'CA', name: 'British Columbia' }, { code: 'CA-MB', countryCode: 'CA', name: 'Manitoba' }, { code: 'CA-NB', countryCode: 'CA', name: 'New Brunswick' }, { code: 'CA-NL', countryCode: 'CA', name: 'Newfoundland and Labrador' }, { code: 'CA-NT', countryCode: 'CA', name: 'Northwest Territories' }, { code: 'CA-NS', countryCode: 'CA', name: 'Nova Scotia' }, { code: 'CA-NU', countryCode: 'CA', name: 'Nunavut' }, { code: 'CA-ON', countryCode: 'CA', name: 'Ontario' }, { code: 'CA-PE', countryCode: 'CA', name: 'Prince Edward Island' }, { code: 'CA-QC', countryCode: 'CA', name: 'Quebec' }, { code: 'CA-SK', countryCode: 'CA', name: 'Saskatchewan' }, { code: 'CA-YT', countryCode: 'CA', name: 'Yukon' }, { code: 'CD-BU', countryCode: 'CD', name: 'Bas-Uele' }, { code: 'CD-EQ', countryCode: 'CD', name: 'Equateur' }, { code: 'CD-HK', countryCode: 'CD', name: 'Haut-Katanga' }, { code: 'CD-HL', countryCode: 'CD', name: 'Haut-Lomani' }, { code: 'CD-HU', countryCode: 'CD', name: 'Haut-Uele' }, { code: 'CD-IT', countryCode: 'CD', name: 'Ituri' }, { code: 'CD-KS', countryCode: 'CD', name: 'Kasai' }, { code: 'CD-KC', countryCode: 'CD', name: 'Kasai Central' }, { code: 'CD-KE', countryCode: 'CD', name: 'Kasai Oriental' }, { code: 'CD-KN', countryCode: 'CD', name: 'Kinshasa' }, { code: 'CD-BC', countryCode: 'CD', name: 'Kongo Central' }, { code: 'CD-KG', countryCode: 'CD', name: 'Kwango' }, { code: 'CD-KL', countryCode: 'CD', name: 'Kwilu' }, { code: 'CD-LO', countryCode: 'CD', name: 'Lomami' }, { code: 'CD-LU', countryCode: 'CD', name: 'Lualaba' }, { code: 'CD-MN', countryCode: 'CD', name: 'Mai-Ndombe' }, { code: 'CD-MA', countryCode: 'CD', name: 'Maniema' }, { code: 'CD-MO', countryCode: 'CD', name: 'Mongala' }, { code: 'CD-NK', countryCode: 'CD', name: 'Nord-Kivu' }, { code: 'CD-NU', countryCode: 'CD', name: 'Nord-Ubangi' }, { code: 'CD-SA', countryCode: 'CD', name: 'Sankuru' }, { code: 'CD-SK', countryCode: 'CD', name: 'Sud-Kivu' }, { code: 'CD-SU', countryCode: 'CD', name: 'Sud-Ubangi' }, { code: 'CD-TA', countryCode: 'CD', name: 'Tanganyika' }, { code: 'CD-TO', countryCode: 'CD', name: 'Tshopo' }, { code: 'CD-TU', countryCode: 'CD', name: 'Tshuapa' }, { code: 'CF-BB', countryCode: 'CF', name: 'Bamingui-Bangoran' }, { code: 'CF-BGF', countryCode: 'CF', name: 'Bangui' }, { code: 'CF-BK', countryCode: 'CF', name: 'Basse-Kotto' }, { code: 'CF-KB', countryCode: 'CF', name: 'Gribingui' }, { code: 'CF-HM', countryCode: 'CF', name: 'Haut-Mbomou' }, { code: 'CF-HK', countryCode: 'CF', name: 'Haute-Kotto' }, { code: 'CF-KG', countryCode: 'CF', name: 'Kemo-Gribingui' }, { code: 'CF-LB', countryCode: 'CF', name: 'Lobaye' }, { code: 'CF-HS', countryCode: 'CF', name: 'Mambere-Kadei' }, { code: 'CF-MB', countryCode: 'CF', name: 'Mbomou' }, { code: 'CF-NM', countryCode: 'CF', name: 'Nana-Mambere' }, { code: 'CF-MP', countryCode: 'CF', name: 'Ombella-Mpoko' }, { code: 'CF-UK', countryCode: 'CF', name: 'Ouaka' }, { code: 'CF-AC', countryCode: 'CF', name: 'Ouham' }, { code: 'CF-OP', countryCode: 'CF', name: 'Ouham-Pende' }, { code: 'CF-SE', countryCode: 'CF', name: 'Sangha' }, { code: 'CG-11', countryCode: 'CG', name: 'Bouenza' }, { code: 'CG-BZV', countryCode: 'CG', name: 'Brazzaville' }, { code: 'CG-8', countryCode: 'CG', name: 'Cuvette' }, { code: 'CG-15', countryCode: 'CG', name: 'Cuvette-Ouest' }, { code: 'CG-2', countryCode: 'CG', name: 'Lekoumou' }, { code: 'CG-7', countryCode: 'CG', name: 'Likouala' }, { code: 'CG-9', countryCode: 'CG', name: 'Niari' }, { code: 'CG-14', countryCode: 'CG', name: 'Plateaux' }, { code: 'CG-16', countryCode: 'CG', name: 'Pointe-Noire' }, { code: 'CG-12', countryCode: 'CG', name: 'Pool' }, { code: 'CG-13', countryCode: 'CG', name: 'Sangha' }, { code: 'CH-AG', countryCode: 'CH', name: 'Aargau' }, { code: 'CH-AR', countryCode: 'CH', name: 'Appenzell Ausserrhoden' }, { code: 'CH-AI', countryCode: 'CH', name: 'Appenzell Innerrhoden' }, { code: 'CH-BL', countryCode: 'CH', name: 'Basel-Landschaft' }, { code: 'CH-BS', countryCode: 'CH', name: 'Basel-Stadt' }, { code: 'CH-BE', countryCode: 'CH', name: 'Bern' }, { code: 'CH-FR', countryCode: 'CH', name: 'Fribourg' }, { code: 'CH-GE', countryCode: 'CH', name: 'Geneve' }, { code: 'CH-GL', countryCode: 'CH', name: 'Glarus' }, { code: 'CH-GR', countryCode: 'CH', name: 'Graubunden' }, { code: 'CH-JU', countryCode: 'CH', name: 'Jura' }, { code: 'CH-LU', countryCode: 'CH', name: 'Luzern' }, { code: 'CH-NE', countryCode: 'CH', name: 'Neuchatel' }, { code: 'CH-NW', countryCode: 'CH', name: 'Nidwalden' }, { code: 'CH-OW', countryCode: 'CH', name: 'Obwalden' }, { code: 'CH-SG', countryCode: 'CH', name: 'Sankt Gallen' }, { code: 'CH-SH', countryCode: 'CH', name: 'Schaffhausen' }, { code: 'CH-SZ', countryCode: 'CH', name: 'Schwyz' }, { code: 'CH-SO', countryCode: 'CH', name: 'Solothurn' }, { code: 'CH-TG', countryCode: 'CH', name: 'Thurgau' }, { code: 'CH-TI', countryCode: 'CH', name: 'Ticino' }, { code: 'CH-UR', countryCode: 'CH', name: 'Uri' }, { code: 'CH-VS', countryCode: 'CH', name: 'Valais' }, { code: 'CH-VD', countryCode: 'CH', name: 'Vaud' }, { code: 'CH-ZG', countryCode: 'CH', name: 'Zug' }, { code: 'CH-ZH', countryCode: 'CH', name: 'Zurich' }, { code: 'CI-AB', countryCode: 'CI', name: 'Abidjan' }, { code: 'CI-BS', countryCode: 'CI', name: 'Bas-Sassandra' }, { code: 'CI-CM', countryCode: 'CI', name: 'Comoe' }, { code: 'CI-DN', countryCode: 'CI', name: 'Denguele' }, { code: 'CI-GD', countryCode: 'CI', name: 'Goh-Djiboua' }, { code: 'CI-LC', countryCode: 'CI', name: 'Lacs' }, { code: 'CI-LG', countryCode: 'CI', name: 'Lagunes' }, { code: 'CI-MG', countryCode: 'CI', name: 'Montagnes' }, { code: 'CI-SM', countryCode: 'CI', name: 'Sassandra-Marahoue' }, { code: 'CI-SV', countryCode: 'CI', name: 'Savanes' }, { code: 'CI-VB', countryCode: 'CI', name: 'Vallee du Bandama' }, { code: 'CI-WR', countryCode: 'CI', name: 'Woroba' }, { code: 'CI-ZZ', countryCode: 'CI', name: 'Zanzan' }, { code: 'CL-AI', countryCode: 'CL', name: 'Aisen del General Carlos Ibanez del Campo' }, { code: 'CL-AN', countryCode: 'CL', name: 'Antofagasta' }, { code: 'CL-AP', countryCode: 'CL', name: 'Arica y Parinacota' }, { code: 'CL-AT', countryCode: 'CL', name: 'Atacama' }, { code: 'CL-BI', countryCode: 'CL', name: 'Biobio' }, { code: 'CL-CO', countryCode: 'CL', name: 'Coquimbo' }, { code: 'CL-AR', countryCode: 'CL', name: 'La Araucania' }, { code: 'CL-LI', countryCode: 'CL', name: "Libertador General Bernardo O'Higgins" }, { code: 'CL-LL', countryCode: 'CL', name: 'Los Lagos' }, { code: 'CL-LR', countryCode: 'CL', name: 'Los Rios' }, { code: 'CL-MA', countryCode: 'CL', name: 'Magallanes' }, { code: 'CL-ML', countryCode: 'CL', name: 'Maule' }, { code: 'CL-RM', countryCode: 'CL', name: 'Region Metropolitana de Santiago' }, { code: 'CL-TA', countryCode: 'CL', name: 'Tarapaca' }, { code: 'CL-VS', countryCode: 'CL', name: 'Valparaiso' }, { code: 'CM-AD', countryCode: 'CM', name: 'Adamaoua' }, { code: 'CM-CE', countryCode: 'CM', name: 'Centre' }, { code: 'CM-ES', countryCode: 'CM', name: 'Est' }, { code: 'CM-EN', countryCode: 'CM', name: 'Extreme-Nord' }, { code: 'CM-LT', countryCode: 'CM', name: 'Littoral' }, { code: 'CM-NO', countryCode: 'CM', name: 'Nord' }, { code: 'CM-NW', countryCode: 'CM', name: 'Nord-Ouest' }, { code: 'CM-OU', countryCode: 'CM', name: 'Ouest' }, { code: 'CM-SU', countryCode: 'CM', name: 'Sud' }, { code: 'CM-SW', countryCode: 'CM', name: 'Sud-Ouest' }, { code: 'CN-34', countryCode: 'CN', name: 'Anhui' }, { code: 'CN-11', countryCode: 'CN', name: 'Beijing' }, { code: 'CN-50', countryCode: 'CN', name: 'Chongqing' }, { code: 'CN-35', countryCode: 'CN', name: 'Fujian' }, { code: 'CN-62', countryCode: 'CN', name: 'Gansu' }, { code: 'CN-44', countryCode: 'CN', name: 'Guangdong' }, { code: 'CN-45', countryCode: 'CN', name: 'Guangxi' }, { code: 'CN-52', countryCode: 'CN', name: 'Guizhou' }, { code: 'CN-46', countryCode: 'CN', name: 'Hainan' }, { code: 'CN-13', countryCode: 'CN', name: 'Hebei' }, { code: 'CN-23', countryCode: 'CN', name: 'Heilongjiang' }, { code: 'CN-41', countryCode: 'CN', name: 'Henan' }, { code: 'CN-42', countryCode: 'CN', name: 'Hubei' }, { code: 'CN-43', countryCode: 'CN', name: 'Hunan' }, { code: 'CN-32', countryCode: 'CN', name: 'Jiangsu' }, { code: 'CN-36', countryCode: 'CN', name: 'Jiangxi' }, { code: 'CN-22', countryCode: 'CN', name: 'Jilin' }, { code: 'CN-21', countryCode: 'CN', name: 'Liaoning' }, { code: 'CN-15', countryCode: 'CN', name: 'Nei Mongol' }, { code: 'CN-64', countryCode: 'CN', name: 'Ningxia' }, { code: 'CN-63', countryCode: 'CN', name: 'Qinghai' }, { code: 'CN-61', countryCode: 'CN', name: 'Shaanxi' }, { code: 'CN-37', countryCode: 'CN', name: 'Shandong' }, { code: 'CN-31', countryCode: 'CN', name: 'Shanghai' }, { code: 'CN-14', countryCode: 'CN', name: 'Shanxi' }, { code: 'CN-51', countryCode: 'CN', name: 'Sichuan' }, { code: 'CN-12', countryCode: 'CN', name: 'Tianjin' }, { code: 'CN-65', countryCode: 'CN', name: 'Xinjiang' }, { code: 'CN-54', countryCode: 'CN', name: 'Xizang' }, { code: 'CN-53', countryCode: 'CN', name: 'Yunnan' }, { code: 'CN-33', countryCode: 'CN', name: 'Zhejiang' }, { code: 'CO-AMA', countryCode: 'CO', name: 'Amazonas' }, { code: 'CO-ANT', countryCode: 'CO', name: 'Antioquia' }, { code: 'CO-ARA', countryCode: 'CO', name: 'Arauca' }, { code: 'CO-ATL', countryCode: 'CO', name: 'Atlantico' }, { code: 'CO-BOL', countryCode: 'CO', name: 'Bolivar' }, { code: 'CO-BOY', countryCode: 'CO', name: 'Boyaca' }, { code: 'CO-CAL', countryCode: 'CO', name: 'Caldas' }, { code: 'CO-CAQ', countryCode: 'CO', name: 'Caqueta' }, { code: 'CO-CAS', countryCode: 'CO', name: 'Casanare' }, { code: 'CO-CAU', countryCode: 'CO', name: 'Cauca' }, { code: 'CO-CES', countryCode: 'CO', name: 'Cesar' }, { code: 'CO-CHO', countryCode: 'CO', name: 'Choco' }, { code: 'CO-COR', countryCode: 'CO', name: 'Cordoba' }, { code: 'CO-CUN', countryCode: 'CO', name: 'Cundinamarca' }, { code: 'CO-DC', countryCode: 'CO', name: 'Distrito Capital de Bogota' }, { code: 'CO-GUA', countryCode: 'CO', name: 'Guainia' }, { code: 'CO-GUV', countryCode: 'CO', name: 'Guaviare' }, { code: 'CO-HUI', countryCode: 'CO', name: 'Huila' }, { code: 'CO-LAG', countryCode: 'CO', name: 'La Guajira' }, { code: 'CO-MAG', countryCode: 'CO', name: 'Magdalena' }, { code: 'CO-MET', countryCode: 'CO', name: 'Meta' }, { code: 'CO-NAR', countryCode: 'CO', name: 'Narino' }, { code: 'CO-NSA', countryCode: 'CO', name: 'Norte de Santander' }, { code: 'CO-PUT', countryCode: 'CO', name: 'Putumayo' }, { code: 'CO-QUI', countryCode: 'CO', name: 'Quindio' }, { code: 'CO-RIS', countryCode: 'CO', name: 'Risaralda' }, { code: 'CO-SAP', countryCode: 'CO', name: 'San Andres, Providencia y Santa Catalina' }, { code: 'CO-SAN', countryCode: 'CO', name: 'Santander' }, { code: 'CO-SUC', countryCode: 'CO', name: 'Sucre' }, { code: 'CO-TOL', countryCode: 'CO', name: 'Tolima' }, { code: 'CO-VAC', countryCode: 'CO', name: 'Valle del Cauca' }, { code: 'CO-VAU', countryCode: 'CO', name: 'Vaupes' }, { code: 'CO-VID', countryCode: 'CO', name: 'Vichada' }, { code: 'CR-A', countryCode: 'CR', name: 'Alajuela' }, { code: 'CR-C', countryCode: 'CR', name: 'Cartago' }, { code: 'CR-G', countryCode: 'CR', name: 'Guanacaste' }, { code: 'CR-H', countryCode: 'CR', name: 'Heredia' }, { code: 'CR-L', countryCode: 'CR', name: 'Limon' }, { code: 'CR-P', countryCode: 'CR', name: 'Puntarenas' }, { code: 'CR-SJ', countryCode: 'CR', name: 'San Jose' }, { code: 'CU-15', countryCode: 'CU', name: 'Artemisa' }, { code: 'CU-09', countryCode: 'CU', name: 'Camaguey' }, { code: 'CU-08', countryCode: 'CU', name: 'Ciego de Avila' }, { code: 'CU-06', countryCode: 'CU', name: 'Cienfuegos' }, { code: 'CU-12', countryCode: 'CU', name: 'Granma' }, { code: 'CU-14', countryCode: 'CU', name: 'Guantanamo' }, { code: 'CU-11', countryCode: 'CU', name: 'Holguin' }, { code: 'CU-99', countryCode: 'CU', name: 'Isla de la Juventud' }, { code: 'CU-03', countryCode: 'CU', name: 'La Habana' }, { code: 'CU-10', countryCode: 'CU', name: 'Las Tunas' }, { code: 'CU-04', countryCode: 'CU', name: 'Matanzas' }, { code: 'CU-16', countryCode: 'CU', name: 'Mayabeque' }, { code: 'CU-01', countryCode: 'CU', name: 'Pinar del Rio' }, { code: 'CU-07', countryCode: 'CU', name: 'Sancti Spiritus' }, { code: 'CU-13', countryCode: 'CU', name: 'Santiago de Cuba' }, { code: 'CU-05', countryCode: 'CU', name: 'Villa Clara' }, { code: 'CV-BV', countryCode: 'CV', name: 'Boa Vista' }, { code: 'CV-BR', countryCode: 'CV', name: 'Brava' }, { code: 'CV-MA', countryCode: 'CV', name: 'Maio' }, { code: 'CV-MO', countryCode: 'CV', name: 'Mosteiros' }, { code: 'CV-PA', countryCode: 'CV', name: 'Paul' }, { code: 'CV-PN', countryCode: 'CV', name: 'Porto Novo' }, { code: 'CV-PR', countryCode: 'CV', name: 'Praia' }, { code: 'CV-RB', countryCode: 'CV', name: 'Ribeira Brava' }, { code: 'CV-RG', countryCode: 'CV', name: 'Ribeira Grande' }, { code: 'CV-RS', countryCode: 'CV', name: 'Ribeira Grande de Santiago' }, { code: 'CV-SL', countryCode: 'CV', name: 'Sal' }, { code: 'CV-CA', countryCode: 'CV', name: 'Santa Catarina' }, { code: 'CV-CF', countryCode: 'CV', name: 'Santa Catarina do Fogo' }, { code: 'CV-CR', countryCode: 'CV', name: 'Santa Cruz' }, { code: 'CV-SD', countryCode: 'CV', name: 'Sao Domingos' }, { code: 'CV-SF', countryCode: 'CV', name: 'Sao Filipe' }, { code: 'CV-SM', countryCode: 'CV', name: 'Sao Miguel' }, { code: 'CV-SS', countryCode: 'CV', name: 'Sao Salvador do Mundo' }, { code: 'CV-SV', countryCode: 'CV', name: 'Sao Vicente' }, { code: 'CV-TA', countryCode: 'CV', name: 'Tarrafal' }, { code: 'CV-TS', countryCode: 'CV', name: 'Tarrafal de Sao Nicolau' }, { code: 'CY-04', countryCode: 'CY', name: 'Ammochostos' }, { code: 'CY-06', countryCode: 'CY', name: 'Keryneia' }, { code: 'CY-03', countryCode: 'CY', name: 'Larnaka' }, { code: 'CY-01', countryCode: 'CY', name: 'Lefkosia' }, { code: 'CY-02', countryCode: 'CY', name: 'Lemesos' }, { code: 'CY-05', countryCode: 'CY', name: 'Pafos' }, { code: 'CZ-JC', countryCode: 'CZ', name: 'Jihocesky kraj' }, { code: 'CZ-JM', countryCode: 'CZ', name: 'Jihomoravsky kraj' }, { code: 'CZ-KA', countryCode: 'CZ', name: 'Karlovarsky kraj' }, { code: 'CZ-63', countryCode: 'CZ', name: 'Kraj Vysocina' }, { code: 'CZ-KR', countryCode: 'CZ', name: 'Kralovehradecky kraj' }, { code: 'CZ-LI', countryCode: 'CZ', name: 'Liberecky kraj' }, { code: 'CZ-MO', countryCode: 'CZ', name: 'Moravskoslezsky kraj' }, { code: 'CZ-OL', countryCode: 'CZ', name: 'Olomoucky kraj' }, { code: 'CZ-PA', countryCode: 'CZ', name: 'Pardubicky kraj' }, { code: 'CZ-PL', countryCode: 'CZ', name: 'Plzensky kraj' }, { code: 'CZ-10', countryCode: 'CZ', name: 'Praha, Hlavni mesto' }, { code: 'CZ-ST', countryCode: 'CZ', name: 'Stredocesky kraj' }, { code: 'CZ-US', countryCode: 'CZ', name: 'Ustecky kraj' }, { code: 'CZ-ZL', countryCode: 'CZ', name: 'Zlinsky kraj' }, { code: 'DE-BW', countryCode: 'DE', name: 'Baden-Wurttemberg' }, { code: 'DE-BY', countryCode: 'DE', name: 'Bayern' }, { code: 'DE-BE', countryCode: 'DE', name: 'Berlin' }, { code: 'DE-BB', countryCode: 'DE', name: 'Brandenburg' }, { code: 'DE-HB', countryCode: 'DE', name: 'Bremen' }, { code: 'DE-HH', countryCode: 'DE', name: 'Hamburg' }, { code: 'DE-HE', countryCode: 'DE', name: 'Hessen' }, { code: 'DE-MV', countryCode: 'DE', name: 'Mecklenburg-Vorpommern' }, { code: 'DE-NI', countryCode: 'DE', name: 'Niedersachsen' }, { code: 'DE-NW', countryCode: 'DE', name: 'Nordrhein-Westfalen' }, { code: 'DE-RP', countryCode: 'DE', name: 'Rheinland-Pfalz' }, { code: 'DE-SL', countryCode: 'DE', name: 'Saarland' }, { code: 'DE-SN', countryCode: 'DE', name: 'Sachsen' }, { code: 'DE-ST', countryCode: 'DE', name: 'Sachsen-Anhalt' }, { code: 'DE-SH', countryCode: 'DE', name: 'Schleswig-Holstein' }, { code: 'DE-TH', countryCode: 'DE', name: 'Thuringen' }, { code: 'DJ-AS', countryCode: 'DJ', name: 'Ali Sabieh' }, { code: 'DJ-AR', countryCode: 'DJ', name: 'Arta' }, { code: 'DJ-DI', countryCode: 'DJ', name: 'Dikhil' }, { code: 'DJ-DJ', countryCode: 'DJ', name: 'Djibouti' }, { code: 'DJ-OB', countryCode: 'DJ', name: 'Obock' }, { code: 'DJ-TA', countryCode: 'DJ', name: 'Tadjourah' }, { code: 'DK-84', countryCode: 'DK', name: 'Hovedstaden' }, { code: 'DK-82', countryCode: 'DK', name: 'Midtjylland' }, { code: 'DK-81', countryCode: 'DK', name: 'Nordjylland' }, { code: 'DK-85', countryCode: 'DK', name: 'Sjelland' }, { code: 'DK-83', countryCode: 'DK', name: 'Syddanmark' }, { code: 'DM-02', countryCode: 'DM', name: 'Saint Andrew' }, { code: 'DM-03', countryCode: 'DM', name: 'Saint David' }, { code: 'DM-04', countryCode: 'DM', name: 'Saint George' }, { code: 'DM-05', countryCode: 'DM', name: 'Saint John' }, { code: 'DM-06', countryCode: 'DM', name: 'Saint Joseph' }, { code: 'DM-07', countryCode: 'DM', name: 'Saint Luke' }, { code: 'DM-08', countryCode: 'DM', name: 'Saint Mark' }, { code: 'DM-09', countryCode: 'DM', name: 'Saint Patrick' }, { code: 'DM-10', countryCode: 'DM', name: 'Saint Paul' }, { code: 'DO-02', countryCode: 'DO', name: 'Azua' }, { code: 'DO-03', countryCode: 'DO', name: 'Baoruco' }, { code: 'DO-04', countryCode: 'DO', name: 'Barahona' }, { code: 'DO-05', countryCode: 'DO', name: 'Dajabon' }, { code: 'DO-01', countryCode: 'DO', name: 'Distrito Nacional (Santo Domingo)' }, { code: 'DO-06', countryCode: 'DO', name: 'Duarte' }, { code: 'DO-08', countryCode: 'DO', name: 'El Seibo' }, { code: 'DO-07', countryCode: 'DO', name: 'Elias Pina' }, { code: 'DO-09', countryCode: 'DO', name: 'Espaillat' }, { code: 'DO-30', countryCode: 'DO', name: 'Hato Mayor' }, { code: 'DO-19', countryCode: 'DO', name: 'Hermanas Mirabal' }, { code: 'DO-10', countryCode: 'DO', name: 'Independencia' }, { code: 'DO-11', countryCode: 'DO', name: 'La Altagracia' }, { code: 'DO-12', countryCode: 'DO', name: 'La Romana' }, { code: 'DO-13', countryCode: 'DO', name: 'La Vega' }, { code: 'DO-14', countryCode: 'DO', name: 'Maria Trinidad Sanchez' }, { code: 'DO-28', countryCode: 'DO', name: 'Monsenor Nouel' }, { code: 'DO-15', countryCode: 'DO', name: 'Monte Cristi' }, { code: 'DO-29', countryCode: 'DO', name: 'Monte Plata' }, { code: 'DO-16', countryCode: 'DO', name: 'Pedernales' }, { code: 'DO-17', countryCode: 'DO', name: 'Peravia' }, { code: 'DO-18', countryCode: 'DO', name: 'Puerto Plata' }, { code: 'DO-20', countryCode: 'DO', name: 'Samana' }, { code: 'DO-21', countryCode: 'DO', name: 'San Cristobal' }, { code: 'DO-22', countryCode: 'DO', name: 'San Juan' }, { code: 'DO-23', countryCode: 'DO', name: 'San Pedro de Macoris' }, { code: 'DO-24', countryCode: 'DO', name: 'Sanchez Ramirez' }, { code: 'DO-25', countryCode: 'DO', name: 'Santiago' }, { code: 'DO-26', countryCode: 'DO', name: 'Santiago Rodriguez' }, { code: 'DO-27', countryCode: 'DO', name: 'Valverde' }, { code: 'DZ-01', countryCode: 'DZ', name: 'Adrar' }, { code: 'DZ-44', countryCode: 'DZ', name: 'Ain Defla' }, { code: 'DZ-46', countryCode: 'DZ', name: 'Ain Temouchent' }, { code: 'DZ-16', countryCode: 'DZ', name: 'Alger' }, { code: 'DZ-23', countryCode: 'DZ', name: 'Annaba' }, { code: 'DZ-05', countryCode: 'DZ', name: 'Batna' }, { code: 'DZ-08', countryCode: 'DZ', name: 'Bechar' }, { code: 'DZ-06', countryCode: 'DZ', name: 'Bejaia' }, { code: 'DZ-07', countryCode: 'DZ', name: 'Biskra' }, { code: 'DZ-09', countryCode: 'DZ', name: 'Blida' }, { code: 'DZ-34', countryCode: 'DZ', name: 'Bordj Bou Arreridj' }, { code: 'DZ-10', countryCode: 'DZ', name: 'Bouira' }, { code: 'DZ-35', countryCode: 'DZ', name: 'Boumerdes' }, { code: 'DZ-02', countryCode: 'DZ', name: 'Chlef' }, { code: 'DZ-25', countryCode: 'DZ', name: 'Constantine' }, { code: 'DZ-17', countryCode: 'DZ', name: 'Djelfa' }, { code: 'DZ-32', countryCode: 'DZ', name: 'El Bayadh' }, { code: 'DZ-39', countryCode: 'DZ', name: 'El Oued' }, { code: 'DZ-36', countryCode: 'DZ', name: 'El Tarf' }, { code: 'DZ-47', countryCode: 'DZ', name: 'Ghardaia' }, { code: 'DZ-24', countryCode: 'DZ', name: 'Guelma' }, { code: 'DZ-33', countryCode: 'DZ', name: 'Illizi' }, { code: 'DZ-40', countryCode: 'DZ', name: 'Khenchela' }, { code: 'DZ-03', countryCode: 'DZ', name: 'Laghouat' }, { code: 'DZ-28', countryCode: 'DZ', name: "M'sila" }, { code: 'DZ-29', countryCode: 'DZ', name: 'Mascara' }, { code: 'DZ-26', countryCode: 'DZ', name: 'Medea' }, { code: 'DZ-43', countryCode: 'DZ', name: 'Mila' }, { code: 'DZ-27', countryCode: 'DZ', name: 'Mostaganem' }, { code: 'DZ-45', countryCode: 'DZ', name: 'Naama' }, { code: 'DZ-31', countryCode: 'DZ', name: 'Oran' }, { code: 'DZ-30', countryCode: 'DZ', name: 'Ouargla' }, { code: 'DZ-04', countryCode: 'DZ', name: 'Oum el Bouaghi' }, { code: 'DZ-48', countryCode: 'DZ', name: 'Relizane' }, { code: 'DZ-20', countryCode: 'DZ', name: 'Saida' }, { code: 'DZ-19', countryCode: 'DZ', name: 'Setif' }, { code: 'DZ-22', countryCode: 'DZ', name: 'Sidi Bel Abbes' }, { code: 'DZ-21', countryCode: 'DZ', name: 'Skikda' }, { code: 'DZ-41', countryCode: 'DZ', name: 'Souk Ahras' }, { code: 'DZ-11', countryCode: 'DZ', name: 'Tamanrasset' }, { code: 'DZ-12', countryCode: 'DZ', name: 'Tebessa' }, { code: 'DZ-14', countryCode: 'DZ', name: 'Tiaret' }, { code: 'DZ-37', countryCode: 'DZ', name: 'Tindouf' }, { code: 'DZ-42', countryCode: 'DZ', name: 'Tipaza' }, { code: 'DZ-38', countryCode: 'DZ', name: 'Tissemsilt' }, { code: 'DZ-15', countryCode: 'DZ', name: 'Tizi Ouzou' }, { code: 'DZ-13', countryCode: 'DZ', name: 'Tlemcen' }, { code: 'EC-A', countryCode: 'EC', name: 'Azuay' }, { code: 'EC-B', countryCode: 'EC', name: 'Bolivar' }, { code: 'EC-F', countryCode: 'EC', name: 'Canar' }, { code: 'EC-C', countryCode: 'EC', name: 'Carchi' }, { code: 'EC-H', countryCode: 'EC', name: 'Chimborazo' }, { code: 'EC-X', countryCode: 'EC', name: 'Cotopaxi' }, { code: 'EC-O', countryCode: 'EC', name: 'El Oro' }, { code: 'EC-E', countryCode: 'EC', name: 'Esmeraldas' }, { code: 'EC-W', countryCode: 'EC', name: 'Galapagos' }, { code: 'EC-G', countryCode: 'EC', name: 'Guayas' }, { code: 'EC-I', countryCode: 'EC', name: 'Imbabura' }, { code: 'EC-L', countryCode: 'EC', name: 'Loja' }, { code: 'EC-R', countryCode: 'EC', name: 'Los Rios' }, { code: 'EC-M', countryCode: 'EC', name: 'Manabi' }, { code: 'EC-S', countryCode: 'EC', name: 'Morona-Santiago' }, { code: 'EC-N', countryCode: 'EC', name: 'Napo' }, { code: 'EC-D', countryCode: 'EC', name: 'Orellana' }, { code: 'EC-Y', countryCode: 'EC', name: 'Pastaza' }, { code: 'EC-P', countryCode: 'EC', name: 'Pichincha' }, { code: 'EC-SE', countryCode: 'EC', name: 'Santa Elena' }, { code: 'EC-U', countryCode: 'EC', name: 'Sucumbios' }, { code: 'EC-T', countryCode: 'EC', name: 'Tungurahua' }, { code: 'EC-Z', countryCode: 'EC', name: 'Zamora-Chinchipe' }, { code: 'EE-37', countryCode: 'EE', name: 'Harjumaa' }, { code: 'EE-39', countryCode: 'EE', name: 'Hiiumaa' }, { code: 'EE-44', countryCode: 'EE', name: 'Ida-Virumaa' }, { code: 'EE-51', countryCode: 'EE', name: 'Jarvamaa' }, { code: 'EE-49', countryCode: 'EE', name: 'Jogevamaa' }, { code: 'EE-59', countryCode: 'EE', name: 'Laane-Virumaa' }, { code: 'EE-57', countryCode: 'EE', name: 'Laanemaa' }, { code: 'EE-67', countryCode: 'EE', name: 'Parnumaa' }, { code: 'EE-65', countryCode: 'EE', name: 'Polvamaa' }, { code: 'EE-70', countryCode: 'EE', name: 'Raplamaa' }, { code: 'EE-74', countryCode: 'EE', name: 'Saaremaa' }, { code: 'EE-78', countryCode: 'EE', name: 'Tartumaa' }, { code: 'EE-82', countryCode: 'EE', name: 'Valgamaa' }, { code: 'EE-84', countryCode: 'EE', name: 'Viljandimaa' }, { code: 'EE-86', countryCode: 'EE', name: 'Vorumaa' }, { code: 'EG-DK', countryCode: 'EG', name: 'Ad Daqahliyah' }, { code: 'EG-BA', countryCode: 'EG', name: 'Al Bahr al Ahmar' }, { code: 'EG-BH', countryCode: 'EG', name: 'Al Buhayrah' }, { code: 'EG-FYM', countryCode: 'EG', name: 'Al Fayyum' }, { code: 'EG-GH', countryCode: 'EG', name: 'Al Gharbiyah' }, { code: 'EG-ALX', countryCode: 'EG', name: 'Al Iskandariyah' }, { code: 'EG-IS', countryCode: 'EG', name: "Al Isma'iliyah" }, { code: 'EG-GZ', countryCode: 'EG', name: 'Al Jizah' }, { code: 'EG-MNF', countryCode: 'EG', name: 'Al Minufiyah' }, { code: 'EG-MN', countryCode: 'EG', name: 'Al Minya' }, { code: 'EG-C', countryCode: 'EG', name: 'Al Qahirah' }, { code: 'EG-KB', countryCode: 'EG', name: 'Al Qalyubiyah' }, { code: 'EG-LX', countryCode: 'EG', name: 'Al Uqsur' }, { code: 'EG-WAD', countryCode: 'EG', name: 'Al Wadi al Jadid' }, { code: 'EG-SUZ', countryCode: 'EG', name: 'As Suways' }, { code: 'EG-SHR', countryCode: 'EG', name: 'Ash Sharqiyah' }, { code: 'EG-ASN', countryCode: 'EG', name: 'Aswan' }, { code: 'EG-AST', countryCode: 'EG', name: 'Asyut' }, { code: 'EG-BNS', countryCode: 'EG', name: 'Bani Suwayf' }, { code: 'EG-PTS', countryCode: 'EG', name: "Bur Sa'id" }, { code: 'EG-DT', countryCode: 'EG', name: 'Dumyat' }, { code: 'EG-JS', countryCode: 'EG', name: "Janub Sina'" }, { code: 'EG-KFS', countryCode: 'EG', name: 'Kafr ash Shaykh' }, { code: 'EG-MT', countryCode: 'EG', name: 'Matruh' }, { code: 'EG-KN', countryCode: 'EG', name: 'Qina' }, { code: 'EG-SIN', countryCode: 'EG', name: "Shamal Sina'" }, { code: 'EG-SHG', countryCode: 'EG', name: 'Suhaj' }, { code: 'ER-MA', countryCode: 'ER', name: 'Al Awsat' }, { code: 'ER-DU', countryCode: 'ER', name: 'Al Janubi' }, { code: 'ER-AN', countryCode: 'ER', name: 'Ansaba' }, { code: 'ER-DK', countryCode: 'ER', name: 'Janubi al Bahri al Ahmar' }, { code: 'ER-GB', countryCode: 'ER', name: 'Qash-Barkah' }, { code: 'ER-SK', countryCode: 'ER', name: 'Shimali al Bahri al Ahmar' }, { code: 'ES-AN', countryCode: 'ES', name: 'Andalucia' }, { code: 'ES-AR', countryCode: 'ES', name: 'Aragon' }, { code: 'ES-AS', countryCode: 'ES', name: 'Asturias, Principado de' }, { code: 'ES-CN', countryCode: 'ES', name: 'Canarias' }, { code: 'ES-CB', countryCode: 'ES', name: 'Cantabria' }, { code: 'ES-CL', countryCode: 'ES', name: 'Castilla y Leon' }, { code: 'ES-CM', countryCode: 'ES', name: 'Castilla-La Mancha' }, { code: 'ES-CT', countryCode: 'ES', name: 'Catalunya' }, { code: 'ES-CE', countryCode: 'ES', name: 'Ceuta' }, { code: 'ES-EX', countryCode: 'ES', name: 'Extremadura' }, { code: 'ES-GA', countryCode: 'ES', name: 'Galicia' }, { code: 'ES-IB', countryCode: 'ES', name: 'Illes Balears' }, { code: 'ES-RI', countryCode: 'ES', name: 'La Rioja' }, { code: 'ES-MD', countryCode: 'ES', name: 'Madrid, Comunidad de' }, { code: 'ES-ML', countryCode: 'ES', name: 'Melilla' }, { code: 'ES-MC', countryCode: 'ES', name: 'Murcia, Region de' }, { code: 'ES-NC', countryCode: 'ES', name: 'Navarra, Comunidad Foral de' }, { code: 'ES-PV', countryCode: 'ES', name: 'Pais Vasco' }, { code: 'ES-VC', countryCode: 'ES', name: 'Valenciana, Comunidad' }, { code: 'ET-AA', countryCode: 'ET', name: 'Adis Abeba' }, { code: 'ET-AF', countryCode: 'ET', name: 'Afar' }, { code: 'ET-AM', countryCode: 'ET', name: 'Amara' }, { code: 'ET-BE', countryCode: 'ET', name: 'Binshangul Gumuz' }, { code: 'ET-DD', countryCode: 'ET', name: 'Dire Dawa' }, { code: 'ET-GA', countryCode: 'ET', name: 'Gambela Hizboch' }, { code: 'ET-HA', countryCode: 'ET', name: 'Hareri Hizb' }, { code: 'ET-OR', countryCode: 'ET', name: 'Oromiya' }, { code: 'ET-SO', countryCode: 'ET', name: 'Sumale' }, { code: 'ET-TI', countryCode: 'ET', name: 'Tigray' }, { code: 'ET-SN', countryCode: 'ET', name: 'YeDebub Biheroch Bihereseboch na Hizboch' }, { code: 'FI-02', countryCode: 'FI', name: 'Etela-Karjala' }, { code: 'FI-03', countryCode: 'FI', name: 'Etela-Pohjanmaa' }, { code: 'FI-04', countryCode: 'FI', name: 'Etela-Savo' }, { code: 'FI-05', countryCode: 'FI', name: 'Kainuu' }, { code: 'FI-06', countryCode: 'FI', name: 'Kanta-Hame' }, { code: 'FI-07', countryCode: 'FI', name: 'Keski-Pohjanmaa' }, { code: 'FI-08', countryCode: 'FI', name: 'Keski-Suomi' }, { code: 'FI-09', countryCode: 'FI', name: 'Kymenlaakso' }, { code: 'FI-10', countryCode: 'FI', name: 'Lappi' }, { code: 'FI-16', countryCode: 'FI', name: 'Paijat-Hame' }, { code: 'FI-11', countryCode: 'FI', name: 'Pirkanmaa' }, { code: 'FI-12', countryCode: 'FI', name: 'Pohjanmaa' }, { code: 'FI-13', countryCode: 'FI', name: 'Pohjois-Karjala' }, { code: 'FI-14', countryCode: 'FI', name: 'Pohjois-Pohjanmaa' }, { code: 'FI-15', countryCode: 'FI', name: 'Pohjois-Savo' }, { code: 'FI-17', countryCode: 'FI', name: 'Satakunta' }, { code: 'FI-18', countryCode: 'FI', name: 'Uusimaa' }, { code: 'FI-19', countryCode: 'FI', name: 'Varsinais-Suomi' }, { code: 'FJ-C', countryCode: 'FJ', name: 'Central' }, { code: 'FJ-N', countryCode: 'FJ', name: 'Northern' }, { code: 'FJ-W', countryCode: 'FJ', name: 'Western' }, { code: 'FM-TRK', countryCode: 'FM', name: 'Chuuk' }, { code: 'FM-KSA', countryCode: 'FM', name: 'Kosrae' }, { code: 'FM-PNI', countryCode: 'FM', name: 'Pohnpei' }, { code: 'FM-YAP', countryCode: 'FM', name: 'Yap' }, { code: 'FR-ARA', countryCode: 'FR', name: 'Auvergne-Rhone-Alpes' }, { code: 'FR-BFC', countryCode: 'FR', name: 'Bourgogne-Franche-Comte' }, { code: 'FR-E', countryCode: 'FR', name: 'Bretagne' }, { code: 'FR-CVL', countryCode: 'FR', name: 'Centre-Val de Loire' }, { code: 'FR-H', countryCode: 'FR', name: 'Corse' }, { code: 'FR-GES', countryCode: 'FR', name: 'Grand-Est' }, { code: 'FR-HDF', countryCode: 'FR', name: 'Hauts-de-France' }, { code: 'FR-J', countryCode: 'FR', name: 'Ile-de-France' }, { code: 'FR-NOR', countryCode: 'FR', name: 'Normandie' }, { code: 'FR-NAQ', countryCode: 'FR', name: 'Nouvelle-Aquitaine' }, { code: 'FR-OCC', countryCode: 'FR', name: 'Occitanie' }, { code: 'FR-R', countryCode: 'FR', name: 'Pays-de-la-Loire' }, { code: 'FR-PAC', countryCode: 'FR', name: "Provence-Alpes-Cote d'Azur" }, { code: 'GA-1', countryCode: 'GA', name: 'Estuaire' }, { code: 'GA-2', countryCode: 'GA', name: 'Haut-Ogooue' }, { code: 'GA-3', countryCode: 'GA', name: 'Moyen-Ogooue' }, { code: 'GA-4', countryCode: 'GA', name: 'Ngounie' }, { code: 'GA-5', countryCode: 'GA', name: 'Nyanga' }, { code: 'GA-6', countryCode: 'GA', name: 'Ogooue-Ivindo' }, { code: 'GA-7', countryCode: 'GA', name: 'Ogooue-Lolo' }, { code: 'GA-8', countryCode: 'GA', name: 'Ogooue-Maritime' }, { code: 'GA-9', countryCode: 'GA', name: 'Woleu-Ntem' }, { code: 'GB-ENG', countryCode: 'GB', name: 'England' }, { code: 'GB-NIR', countryCode: 'GB', name: 'Northern Ireland' }, { code: 'GB-SCT', countryCode: 'GB', name: 'Scotland' }, { code: 'GB-WLS', countryCode: 'GB', name: 'Wales' }, { code: 'GD-01', countryCode: 'GD', name: 'Saint Andrew' }, { code: 'GD-02', countryCode: 'GD', name: 'Saint David' }, { code: 'GD-03', countryCode: 'GD', name: 'Saint George' }, { code: 'GD-04', countryCode: 'GD', name: 'Saint John' }, { code: 'GD-05', countryCode: 'GD', name: 'Saint Mark' }, { code: 'GD-06', countryCode: 'GD', name: 'Saint Patrick' }, { code: 'GE-AB', countryCode: 'GE', name: 'Abkhazia' }, { code: 'GE-AJ', countryCode: 'GE', name: 'Ajaria' }, { code: 'GE-GU', countryCode: 'GE', name: 'Guria' }, { code: 'GE-IM', countryCode: 'GE', name: 'Imereti' }, { code: 'GE-KA', countryCode: 'GE', name: "K'akheti" }, { code: 'GE-KK', countryCode: 'GE', name: 'Kvemo Kartli' }, { code: 'GE-MM', countryCode: 'GE', name: 'Mtskheta-Mtianeti' }, { code: 'GE-RL', countryCode: 'GE', name: "Rach'a-Lechkhumi-Kvemo Svaneti" }, { code: 'GE-SZ', countryCode: 'GE', name: 'Samegrelo-Zemo Svaneti' }, { code: 'GE-SJ', countryCode: 'GE', name: 'Samtskhe-Javakheti' }, { code: 'GE-SK', countryCode: 'GE', name: 'Shida Kartli' }, { code: 'GE-TB', countryCode: 'GE', name: 'Tbilisi' }, { code: 'GH-AH', countryCode: 'GH', name: 'Ashanti' }, { code: 'GH-BA', countryCode: 'GH', name: 'Brong-Ahafo' }, { code: 'GH-CP', countryCode: 'GH', name: 'Central' }, { code: 'GH-EP', countryCode: 'GH', name: 'Eastern' }, { code: 'GH-AA', countryCode: 'GH', name: 'Greater Accra' }, { code: 'GH-NP', countryCode: 'GH', name: 'Northern' }, { code: 'GH-UE', countryCode: 'GH', name: 'Upper East' }, { code: 'GH-UW', countryCode: 'GH', name: 'Upper West' }, { code: 'GH-TV', countryCode: 'GH', name: 'Volta' }, { code: 'GH-WP', countryCode: 'GH', name: 'Western' }, { code: 'GL-KU', countryCode: 'GL', name: 'Kommune Kujalleq' }, { code: 'GL-SM', countryCode: 'GL', name: 'Kommuneqarfik Sermersooq' }, { code: 'GL-QA', countryCode: 'GL', name: 'Qaasuitsup Kommunia' }, { code: 'GL-QE', countryCode: 'GL', name: 'Qeqqata Kommunia' }, { code: 'GM-B', countryCode: 'GM', name: 'Banjul' }, { code: 'GM-M', countryCode: 'GM', name: 'Central River' }, { code: 'GM-L', countryCode: 'GM', name: 'Lower River' }, { code: 'GM-N', countryCode: 'GM', name: 'North Bank' }, { code: 'GM-U', countryCode: 'GM', name: 'Upper River' }, { code: 'GM-W', countryCode: 'GM', name: 'Western' }, { code: 'GN-BE', countryCode: 'GN', name: 'Beyla' }, { code: 'GN-BF', countryCode: 'GN', name: 'Boffa' }, { code: 'GN-B', countryCode: 'GN', name: 'Boke' }, { code: 'GN-C', countryCode: 'GN', name: 'Conakry' }, { code: 'GN-CO', countryCode: 'GN', name: 'Coyah' }, { code: 'GN-DB', countryCode: 'GN', name: 'Dabola' }, { code: 'GN-DL', countryCode: 'GN', name: 'Dalaba' }, { code: 'GN-DI', countryCode: 'GN', name: 'Dinguiraye' }, { code: 'GN-DU', countryCode: 'GN', name: 'Dubreka' }, { code: 'GN-F', countryCode: 'GN', name: 'Faranah' }, { code: 'GN-FO', countryCode: 'GN', name: 'Forecariah' }, { code: 'GN-FR', countryCode: 'GN', name: 'Fria' }, { code: 'GN-GA', countryCode: 'GN', name: 'Gaoual' }, { code: 'GN-GU', countryCode: 'GN', name: 'Guekedou' }, { code: 'GN-K', countryCode: 'GN', name: 'Kankan' }, { code: 'GN-KE', countryCode: 'GN', name: 'Kerouane' }, { code: 'GN-D', countryCode: 'GN', name: 'Kindia' }, { code: 'GN-KS', countryCode: 'GN', name: 'Kissidougou' }, { code: 'GN-KB', countryCode: 'GN', name: 'Koubia' }, { code: 'GN-KN', countryCode: 'GN', name: 'Koundara' }, { code: 'GN-KO', countryCode: 'GN', name: 'Kouroussa' }, { code: 'GN-L', countryCode: 'GN', name: 'Labe' }, { code: 'GN-LE', countryCode: 'GN', name: 'Lelouma' }, { code: 'GN-LO', countryCode: 'GN', name: 'Lola' }, { code: 'GN-MC', countryCode: 'GN', name: 'Macenta' }, { code: 'GN-ML', countryCode: 'GN', name: 'Mali' }, { code: 'GN-M', countryCode: 'GN', name: 'Mamou' }, { code: 'GN-MD', countryCode: 'GN', name: 'Mandiana' }, { code: 'GN-N', countryCode: 'GN', name: 'Nzerekore' }, { code: 'GN-PI', countryCode: 'GN', name: 'Pita' }, { code: 'GN-SI', countryCode: 'GN', name: 'Siguiri' }, { code: 'GN-TE', countryCode: 'GN', name: 'Telimele' }, { code: 'GN-TO', countryCode: 'GN', name: 'Tougue' }, { code: 'GN-YO', countryCode: 'GN', name: 'Yomou' }, { code: 'GQ-AN', countryCode: 'GQ', name: 'Annobon' }, { code: 'GQ-BN', countryCode: 'GQ', name: 'Bioko Norte' }, { code: 'GQ-BS', countryCode: 'GQ', name: 'Bioko Sur' }, { code: 'GQ-CS', countryCode: 'GQ', name: 'Centro Sur' }, { code: 'GQ-KN', countryCode: 'GQ', name: 'Kie-Ntem' }, { code: 'GQ-LI', countryCode: 'GQ', name: 'Litoral' }, { code: 'GQ-WN', countryCode: 'GQ', name: 'Wele-Nzas' }, { code: 'GR-A', countryCode: 'GR', name: 'Anatoliki Makedonia kai Thraki' }, { code: 'GR-I', countryCode: 'GR', name: 'Attiki' }, { code: 'GR-G', countryCode: 'GR', name: 'Dytiki Ellada' }, { code: 'GR-C', countryCode: 'GR', name: 'Dytiki Makedonia' }, { code: 'GR-F', countryCode: 'GR', name: 'Ionia Nisia' }, { code: 'GR-D', countryCode: 'GR', name: 'Ipeiros' }, { code: 'GR-B', countryCode: 'GR', name: 'Kentriki Makedonia' }, { code: 'GR-M', countryCode: 'GR', name: 'Kriti' }, { code: 'GR-L', countryCode: 'GR', name: 'Notio Aigaio' }, { code: 'GR-J', countryCode: 'GR', name: 'Peloponnisos' }, { code: 'GR-H', countryCode: 'GR', name: 'Sterea Ellada' }, { code: 'GR-E', countryCode: 'GR', name: 'Thessalia' }, { code: 'GR-K', countryCode: 'GR', name: 'Voreio Aigaio' }, { code: 'GT-AV', countryCode: 'GT', name: 'Alta Verapaz' }, { code: 'GT-BV', countryCode: 'GT', name: 'Baja Verapaz' }, { code: 'GT-CM', countryCode: 'GT', name: 'Chimaltenango' }, { code: 'GT-CQ', countryCode: 'GT', name: 'Chiquimula' }, { code: 'GT-PR', countryCode: 'GT', name: 'El Progreso' }, { code: 'GT-ES', countryCode: 'GT', name: 'Escuintla' }, { code: 'GT-GU', countryCode: 'GT', name: 'Guatemala' }, { code: 'GT-HU', countryCode: 'GT', name: 'Huehuetenango' }, { code: 'GT-IZ', countryCode: 'GT', name: 'Izabal' }, { code: 'GT-JA', countryCode: 'GT', name: 'Jalapa' }, { code: 'GT-JU', countryCode: 'GT', name: 'Jutiapa' }, { code: 'GT-PE', countryCode: 'GT', name: 'Peten' }, { code: 'GT-QZ', countryCode: 'GT', name: 'Quetzaltenango' }, { code: 'GT-QC', countryCode: 'GT', name: 'Quiche' }, { code: 'GT-RE', countryCode: 'GT', name: 'Retalhuleu' }, { code: 'GT-SA', countryCode: 'GT', name: 'Sacatepequez' }, { code: 'GT-SM', countryCode: 'GT', name: 'San Marcos' }, { code: 'GT-SR', countryCode: 'GT', name: 'Santa Rosa' }, { code: 'GT-SO', countryCode: 'GT', name: 'Solola' }, { code: 'GT-SU', countryCode: 'GT', name: 'Suchitepequez' }, { code: 'GT-TO', countryCode: 'GT', name: 'Totonicapan' }, { code: 'GT-ZA', countryCode: 'GT', name: 'Zacapa' }, { code: 'GW-BA', countryCode: 'GW', name: 'Bafata' }, { code: 'GW-BM', countryCode: 'GW', name: 'Biombo' }, { code: 'GW-BS', countryCode: 'GW', name: 'Bissau' }, { code: 'GW-BL', countryCode: 'GW', name: 'Bolama' }, { code: 'GW-CA', countryCode: 'GW', name: 'Cacheu' }, { code: 'GW-GA', countryCode: 'GW', name: 'Gabu' }, { code: 'GW-OI', countryCode: 'GW', name: 'Oio' }, { code: 'GW-QU', countryCode: 'GW', name: 'Quinara' }, { code: 'GW-TO', countryCode: 'GW', name: 'Tombali' }, { code: 'GY-CU', countryCode: 'GY', name: 'Cuyuni-Mazaruni' }, { code: 'GY-DE', countryCode: 'GY', name: 'Demerara-Mahaica' }, { code: 'GY-EB', countryCode: 'GY', name: 'East Berbice-Corentyne' }, { code: 'GY-ES', countryCode: 'GY', name: 'Essequibo Islands-West Demerara' }, { code: 'GY-MA', countryCode: 'GY', name: 'Mahaica-Berbice' }, { code: 'GY-PM', countryCode: 'GY', name: 'Pomeroon-Supenaam' }, { code: 'GY-UD', countryCode: 'GY', name: 'Upper Demerara-Berbice' }, { code: 'HN-AT', countryCode: 'HN', name: 'Atlantida' }, { code: 'HN-CH', countryCode: 'HN', name: 'Choluteca' }, { code: 'HN-CL', countryCode: 'HN', name: 'Colon' }, { code: 'HN-CM', countryCode: 'HN', name: 'Comayagua' }, { code: 'HN-CP', countryCode: 'HN', name: 'Copan' }, { code: 'HN-CR', countryCode: 'HN', name: 'Cortes' }, { code: 'HN-EP', countryCode: 'HN', name: 'El Paraiso' }, { code: 'HN-FM', countryCode: 'HN', name: 'Francisco Morazan' }, { code: 'HN-GD', countryCode: 'HN', name: 'Gracias a Dios' }, { code: 'HN-IN', countryCode: 'HN', name: 'Intibuca' }, { code: 'HN-IB', countryCode: 'HN', name: 'Islas de la Bahia' }, { code: 'HN-LP', countryCode: 'HN', name: 'La Paz' }, { code: 'HN-LE', countryCode: 'HN', name: 'Lempira' }, { code: 'HN-OC', countryCode: 'HN', name: 'Ocotepeque' }, { code: 'HN-OL', countryCode: 'HN', name: 'Olancho' }, { code: 'HN-SB', countryCode: 'HN', name: 'Santa Barbara' }, { code: 'HN-VA', countryCode: 'HN', name: 'Valle' }, { code: 'HN-YO', countryCode: 'HN', name: 'Yoro' }, { code: 'HR-07', countryCode: 'HR', name: 'Bjelovarsko-bilogorska zupanija' }, { code: 'HR-12', countryCode: 'HR', name: 'Brodsko-posavska zupanija' }, { code: 'HR-19', countryCode: 'HR', name: 'Dubrovacko-neretvanska zupanija' }, { code: 'HR-21', countryCode: 'HR', name: 'Grad Zagreb' }, { code: 'HR-18', countryCode: 'HR', name: 'Istarska zupanija' }, { code: 'HR-04', countryCode: 'HR', name: 'Karlovacka zupanija' }, { code: 'HR-06', countryCode: 'HR', name: 'Koprivnicko-krizevacka zupanija' }, { code: 'HR-02', countryCode: 'HR', name: 'Krapinsko-zagorska zupanija' }, { code: 'HR-09', countryCode: 'HR', name: 'Licko-senjska zupanija' }, { code: 'HR-20', countryCode: 'HR', name: 'Medimurska zupanija' }, { code: 'HR-14', countryCode: 'HR', name: 'Osjecko-baranjska zupanija' }, { code: 'HR-11', countryCode: 'HR', name: 'Pozesko-slavonska zupanija' }, { code: 'HR-08', countryCode: 'HR', name: 'Primorsko-goranska zupanija' }, { code: 'HR-15', countryCode: 'HR', name: 'Sibensko-kninska zupanija' }, { code: 'HR-03', countryCode: 'HR', name: 'Sisacko-moslavacka zupanija' }, { code: 'HR-17', countryCode: 'HR', name: 'Splitsko-dalmatinska zupanija' }, { code: 'HR-05', countryCode: 'HR', name: 'Varazdinska zupanija' }, { code: 'HR-10', countryCode: 'HR', name: 'Viroviticko-podravska zupanija' }, { code: 'HR-16', countryCode: 'HR', name: 'Vukovarsko-srijemska zupanija' }, { code: 'HR-13', countryCode: 'HR', name: 'Zadarska zupanija' }, { code: 'HR-01', countryCode: 'HR', name: 'Zagrebacka zupanija' }, { code: 'HT-AR', countryCode: 'HT', name: 'Artibonite' }, { code: 'HT-CE', countryCode: 'HT', name: 'Centre' }, { code: 'HT-GA', countryCode: 'HT', name: "Grande'Anse" }, { code: 'HT-NI', countryCode: 'HT', name: 'Nippes' }, { code: 'HT-ND', countryCode: 'HT', name: 'Nord' }, { code: 'HT-NE', countryCode: 'HT', name: 'Nord-Est' }, { code: 'HT-NO', countryCode: 'HT', name: 'Nord-Ouest' }, { code: 'HT-OU', countryCode: 'HT', name: 'Ouest' }, { code: 'HT-SD', countryCode: 'HT', name: 'Sud' }, { code: 'HT-SE', countryCode: 'HT', name: 'Sud-Est' }, { code: 'HU-BK', countryCode: 'HU', name: 'Bacs-Kiskun' }, { code: 'HU-BA', countryCode: 'HU', name: 'Baranya' }, { code: 'HU-BE', countryCode: 'HU', name: 'Bekes' }, { code: 'HU-BZ', countryCode: 'HU', name: 'Borsod-Abauj-Zemplen' }, { code: 'HU-BU', countryCode: 'HU', name: 'Budapest' }, { code: 'HU-CS', countryCode: 'HU', name: 'Csongrad' }, { code: 'HU-FE', countryCode: 'HU', name: 'Fejer' }, { code: 'HU-GS', countryCode: 'HU', name: 'Gyor-Moson-Sopron' }, { code: 'HU-HB', countryCode: 'HU', name: 'Hajdu-Bihar' }, { code: 'HU-HE', countryCode: 'HU', name: 'Heves' }, { code: 'HU-JN', countryCode: 'HU', name: 'Jasz-Nagykun-Szolnok' }, { code: 'HU-KE', countryCode: 'HU', name: 'Komarom-Esztergom' }, { code: 'HU-NO', countryCode: 'HU', name: 'Nograd' }, { code: 'HU-PE', countryCode: 'HU', name: 'Pest' }, { code: 'HU-SO', countryCode: 'HU', name: 'Somogy' }, { code: 'HU-SZ', countryCode: 'HU', name: 'Szabolcs-Szatmar-Bereg' }, { code: 'HU-TO', countryCode: 'HU', name: 'Tolna' }, { code: 'HU-VA', countryCode: 'HU', name: 'Vas' }, { code: 'HU-VM', countryCode: 'HU', name: 'Veszprem' }, { code: 'HU-ZA', countryCode: 'HU', name: 'Zala' }, { code: 'ID-AC', countryCode: 'ID', name: 'Aceh' }, { code: 'ID-BA', countryCode: 'ID', name: 'Bali' }, { code: 'ID-BT', countryCode: 'ID', name: 'Banten' }, { code: 'ID-BE', countryCode: 'ID', name: 'Bengkulu' }, { code: 'ID-GO', countryCode: 'ID', name: 'Gorontalo' }, { code: 'ID-JK', countryCode: 'ID', name: 'Jakarta Raya' }, { code: 'ID-JA', countryCode: 'ID', name: 'Jambi' }, { code: 'ID-JB', countryCode: 'ID', name: 'Jawa Barat' }, { code: 'ID-JT', countryCode: 'ID', name: 'Jawa Tengah' }, { code: 'ID-JI', countryCode: 'ID', name: 'Jawa Timur' }, { code: 'ID-KB', countryCode: 'ID', name: 'Kalimantan Barat' }, { code: 'ID-KS', countryCode: 'ID', name: 'Kalimantan Selatan' }, { code: 'ID-KT', countryCode: 'ID', name: 'Kalimantan Tengah' }, { code: 'ID-KI', countryCode: 'ID', name: 'Kalimantan Timur' }, { code: 'ID-BB', countryCode: 'ID', name: 'Kepulauan Bangka Belitung' }, { code: 'ID-KR', countryCode: 'ID', name: 'Kepulauan Riau' }, { code: 'ID-LA', countryCode: 'ID', name: 'Lampung' }, { code: 'ID-ML', countryCode: 'ID', name: 'Maluku' }, { code: 'ID-MU', countryCode: 'ID', name: 'Maluku Utara' }, { code: 'ID-NB', countryCode: 'ID', name: 'Nusa Tenggara Barat' }, { code: 'ID-NT', countryCode: 'ID', name: 'Nusa Tenggara Timur' }, { code: 'ID-PP', countryCode: 'ID', name: 'Papua' }, { code: 'ID-PB', countryCode: 'ID', name: 'Papua Barat' }, { code: 'ID-RI', countryCode: 'ID', name: 'Riau' }, { code: 'ID-SR', countryCode: 'ID', name: 'Sulawesi Barat' }, { code: 'ID-SN', countryCode: 'ID', name: 'Sulawesi Selatan' }, { code: 'ID-ST', countryCode: 'ID', name: 'Sulawesi Tengah' }, { code: 'ID-SG', countryCode: 'ID', name: 'Sulawesi Tenggara' }, { code: 'ID-SA', countryCode: 'ID', name: 'Sulawesi Utara' }, { code: 'ID-SB', countryCode: 'ID', name: 'Sumatera Barat' }, { code: 'ID-SS', countryCode: 'ID', name: 'Sumatera Selatan' }, { code: 'ID-SU', countryCode: 'ID', name: 'Sumatera Utara' }, { code: 'ID-YO', countryCode: 'ID', name: 'Yogyakarta' }, { code: 'IE-CW', countryCode: 'IE', name: 'Carlow' }, { code: 'IE-CN', countryCode: 'IE', name: 'Cavan' }, { code: 'IE-CE', countryCode: 'IE', name: 'Clare' }, { code: 'IE-CO', countryCode: 'IE', name: 'Cork' }, { code: 'IE-DL', countryCode: 'IE', name: 'Donegal' }, { code: 'IE-D', countryCode: 'IE', name: 'Dublin' }, { code: 'IE-G', countryCode: 'IE', name: 'Galway' }, { code: 'IE-KY', countryCode: 'IE', name: 'Kerry' }, { code: 'IE-KE', countryCode: 'IE', name: 'Kildare' }, { code: 'IE-KK', countryCode: 'IE', name: 'Kilkenny' }, { code: 'IE-LS', countryCode: 'IE', name: 'Laois' }, { code: 'IE-LM', countryCode: 'IE', name: 'Leitrim' }, { code: 'IE-LK', countryCode: 'IE', name: 'Limerick' }, { code: 'IE-LD', countryCode: 'IE', name: 'Longford' }, { code: 'IE-LH', countryCode: 'IE', name: 'Louth' }, { code: 'IE-MO', countryCode: 'IE', name: 'Mayo' }, { code: 'IE-MH', countryCode: 'IE', name: 'Meath' }, { code: 'IE-MN', countryCode: 'IE', name: 'Monaghan' }, { code: 'IE-OY', countryCode: 'IE', name: 'Offaly' }, { code: 'IE-RN', countryCode: 'IE', name: 'Roscommon' }, { code: 'IE-SO', countryCode: 'IE', name: 'Sligo' }, { code: 'IE-TA', countryCode: 'IE', name: 'Tipperary' }, { code: 'IE-WD', countryCode: 'IE', name: 'Waterford' }, { code: 'IE-WH', countryCode: 'IE', name: 'Westmeath' }, { code: 'IE-WX', countryCode: 'IE', name: 'Wexford' }, { code: 'IE-WW', countryCode: 'IE', name: 'Wicklow' }, { code: 'IL-D', countryCode: 'IL', name: 'HaDarom' }, { code: 'IL-M', countryCode: 'IL', name: 'HaMerkaz' }, { code: 'IL-Z', countryCode: 'IL', name: 'HaTsafon' }, { code: 'IL-HA', countryCode: 'IL', name: 'Hefa' }, { code: 'IL-TA', countryCode: 'IL', name: 'Tel Aviv' }, { code: 'IL-JM', countryCode: 'IL', name: 'Yerushalayim' }, { code: 'IN-AN', countryCode: 'IN', name: 'Andaman and Nicobar Islands' }, { code: 'IN-AP', countryCode: 'IN', name: 'Andhra Pradesh' }, { code: 'IN-AR', countryCode: 'IN', name: 'Arunachal Pradesh' }, { code: 'IN-AS', countryCode: 'IN', name: 'Assam' }, { code: 'IN-BR', countryCode: 'IN', name: 'Bihar' }, { code: 'IN-CH', countryCode: 'IN', name: 'Chandigarh' }, { code: 'IN-CT', countryCode: 'IN', name: 'Chhattisgarh' }, { code: 'IN-DN', countryCode: 'IN', name: 'Dadra and Nagar Haveli' }, { code: 'IN-DD', countryCode: 'IN', name: 'Daman and Diu' }, { code: 'IN-DL', countryCode: 'IN', name: 'Delhi' }, { code: 'IN-GA', countryCode: 'IN', name: 'Goa' }, { code: 'IN-GJ', countryCode: 'IN', name: 'Gujarat' }, { code: 'IN-HR', countryCode: 'IN', name: 'Haryana' }, { code: 'IN-HP', countryCode: 'IN', name: 'Himachal Pradesh' }, { code: 'IN-JK', countryCode: 'IN', name: 'Jammu and Kashmir' }, { code: 'IN-JH', countryCode: 'IN', name: 'Jharkhand' }, { code: 'IN-KA', countryCode: 'IN', name: 'Karnataka' }, { code: 'IN-KL', countryCode: 'IN', name: 'Kerala' }, { code: 'IN-LD', countryCode: 'IN', name: 'Lakshadweep' }, { code: 'IN-MP', countryCode: 'IN', name: 'Madhya Pradesh' }, { code: 'IN-MH', countryCode: 'IN', name: 'Maharashtra' }, { code: 'IN-MN', countryCode: 'IN', name: 'Manipur' }, { code: 'IN-ML', countryCode: 'IN', name: 'Meghalaya' }, { code: 'IN-MZ', countryCode: 'IN', name: 'Mizoram' }, { code: 'IN-NL', countryCode: 'IN', name: 'Nagaland' }, { code: 'IN-OR', countryCode: 'IN', name: 'Odisha' }, { code: 'IN-PY', countryCode: 'IN', name: 'Puducherry' }, { code: 'IN-PB', countryCode: 'IN', name: 'Punjab' }, { code: 'IN-RJ', countryCode: 'IN', name: 'Rajasthan' }, { code: 'IN-SK', countryCode: 'IN', name: 'Sikkim' }, { code: 'IN-TN', countryCode: 'IN', name: 'Tamil Nadu' }, { code: 'IN-TG', countryCode: 'IN', name: 'Telangana' }, { code: 'IN-TR', countryCode: 'IN', name: 'Tripura' }, { code: 'IN-UP', countryCode: 'IN', name: 'Uttar Pradesh' }, { code: 'IN-UT', countryCode: 'IN', name: 'Uttarakhand' }, { code: 'IN-WB', countryCode: 'IN', name: 'West Bengal' }, { code: 'IQ-AN', countryCode: 'IQ', name: 'Al Anbar' }, { code: 'IQ-BA', countryCode: 'IQ', name: 'Al Basrah' }, { code: 'IQ-MU', countryCode: 'IQ', name: 'Al Muthanna' }, { code: 'IQ-QA', countryCode: 'IQ', name: 'Al Qadisiyah' }, { code: 'IQ-NA', countryCode: 'IQ', name: 'An Najaf' }, { code: 'IQ-AR', countryCode: 'IQ', name: 'Arbil' }, { code: 'IQ-SU', countryCode: 'IQ', name: 'As Sulaymaniyah' }, { code: 'IQ-BB', countryCode: 'IQ', name: 'Babil' }, { code: 'IQ-BG', countryCode: 'IQ', name: 'Baghdad' }, { code: 'IQ-DA', countryCode: 'IQ', name: 'Dahuk' }, { code: 'IQ-DQ', countryCode: 'IQ', name: 'Dhi Qar' }, { code: 'IQ-DI', countryCode: 'IQ', name: 'Diyala' }, { code: 'IQ-KA', countryCode: 'IQ', name: "Karbala'" }, { code: 'IQ-KI', countryCode: 'IQ', name: 'Kirkuk' }, { code: 'IQ-MA', countryCode: 'IQ', name: 'Maysan' }, { code: 'IQ-NI', countryCode: 'IQ', name: 'Ninawa' }, { code: 'IQ-SD', countryCode: 'IQ', name: 'Salah ad Din' }, { code: 'IQ-WA', countryCode: 'IQ', name: 'Wasit' }, { code: 'IR-32', countryCode: 'IR', name: 'Alborz' }, { code: 'IR-03', countryCode: 'IR', name: 'Ardabil' }, { code: 'IR-02', countryCode: 'IR', name: 'Azarbayjan-e Gharbi' }, { code: 'IR-01', countryCode: 'IR', name: 'Azarbayjan-e Sharqi' }, { code: 'IR-06', countryCode: 'IR', name: 'Bushehr' }, { code: 'IR-08', countryCode: 'IR', name: 'Chahar Mahal va Bakhtiari' }, { code: 'IR-04', countryCode: 'IR', name: 'Esfahan' }, { code: 'IR-14', countryCode: 'IR', name: 'Fars' }, { code: 'IR-19', countryCode: 'IR', name: 'Gilan' }, { code: 'IR-27', countryCode: 'IR', name: 'Golestan' }, { code: 'IR-24', countryCode: 'IR', name: 'Hamadan' }, { code: 'IR-23', countryCode: 'IR', name: 'Hormozgan' }, { code: 'IR-05', countryCode: 'IR', name: 'Ilam' }, { code: 'IR-15', countryCode: 'IR', name: 'Kerman' }, { code: 'IR-17', countryCode: 'IR', name: 'Kermanshah' }, { code: 'IR-29', countryCode: 'IR', name: 'Khorasan-e Jonubi' }, { code: 'IR-30', countryCode: 'IR', name: 'Khorasan-e Razavi' }, { code: 'IR-31', countryCode: 'IR', name: 'Khorasan-e Shomali' }, { code: 'IR-10', countryCode: 'IR', name: 'Khuzestan' }, { code: 'IR-18', countryCode: 'IR', name: 'Kohgiluyeh va Bowyer Ahmad' }, { code: 'IR-16', countryCode: 'IR', name: 'Kordestan' }, { code: 'IR-20', countryCode: 'IR', name: 'Lorestan' }, { code: 'IR-22', countryCode: 'IR', name: 'Markazi' }, { code: 'IR-21', countryCode: 'IR', name: 'Mazandaran' }, { code: 'IR-28', countryCode: 'IR', name: 'Qazvin' }, { code: 'IR-26', countryCode: 'IR', name: 'Qom' }, { code: 'IR-12', countryCode: 'IR', name: 'Semnan' }, { code: 'IR-13', countryCode: 'IR', name: 'Sistan va Baluchestan' }, { code: 'IR-07', countryCode: 'IR', name: 'Tehran' }, { code: 'IR-25', countryCode: 'IR', name: 'Yazd' }, { code: 'IR-11', countryCode: 'IR', name: 'Zanjan' }, { code: 'IS-7', countryCode: 'IS', name: 'Austurland' }, { code: 'IS-1', countryCode: 'IS', name: 'Hofudborgarsvaedi utan Reykjavikur' }, { code: 'IS-6', countryCode: 'IS', name: 'Nordurland eystra' }, { code: 'IS-5', countryCode: 'IS', name: 'Nordurland vestra' }, { code: 'IS-8', countryCode: 'IS', name: 'Sudurland' }, { code: 'IS-2', countryCode: 'IS', name: 'Sudurnes' }, { code: 'IS-4', countryCode: 'IS', name: 'Vestfirdir' }, { code: 'IS-3', countryCode: 'IS', name: 'Vesturland' }, { code: 'IT-65', countryCode: 'IT', name: 'Abruzzo' }, { code: 'IT-77', countryCode: 'IT', name: 'Basilicata' }, { code: 'IT-78', countryCode: 'IT', name: 'Calabria' }, { code: 'IT-72', countryCode: 'IT', name: 'Campania' }, { code: 'IT-45', countryCode: 'IT', name: 'Emilia-Romagna' }, { code: 'IT-36', countryCode: 'IT', name: 'Friuli-Venezia Giulia' }, { code: 'IT-62', countryCode: 'IT', name: 'Lazio' }, { code: 'IT-42', countryCode: 'IT', name: 'Liguria' }, { code: 'IT-25', countryCode: 'IT', name: 'Lombardia' }, { code: 'IT-57', countryCode: 'IT', name: 'Marche' }, { code: 'IT-67', countryCode: 'IT', name: 'Molise' }, { code: 'IT-21', countryCode: 'IT', name: 'Piemonte' }, { code: 'IT-75', countryCode: 'IT', name: 'Puglia' }, { code: 'IT-88', countryCode: 'IT', name: 'Sardegna' }, { code: 'IT-82', countryCode: 'IT', name: 'Sicilia' }, { code: 'IT-52', countryCode: 'IT', name: 'Toscana' }, { code: 'IT-32', countryCode: 'IT', name: 'Trentino-Alto Adige' }, { code: 'IT-55', countryCode: 'IT', name: 'Umbria' }, { code: 'IT-23', countryCode: 'IT', name: "Valle d'Aosta" }, { code: 'IT-34', countryCode: 'IT', name: 'Veneto' }, { code: 'JM-13', countryCode: 'JM', name: 'Clarendon' }, { code: 'JM-09', countryCode: 'JM', name: 'Hanover' }, { code: 'JM-01', countryCode: 'JM', name: 'Kingston' }, { code: 'JM-12', countryCode: 'JM', name: 'Manchester' }, { code: 'JM-04', countryCode: 'JM', name: 'Portland' }, { code: 'JM-02', countryCode: 'JM', name: 'Saint Andrew' }, { code: 'JM-06', countryCode: 'JM', name: 'Saint Ann' }, { code: 'JM-14', countryCode: 'JM', name: 'Saint Catherine' }, { code: 'JM-11', countryCode: 'JM', name: 'Saint Elizabeth' }, { code: 'JM-08', countryCode: 'JM', name: 'Saint James' }, { code: 'JM-05', countryCode: 'JM', name: 'Saint Mary' }, { code: 'JM-03', countryCode: 'JM', name: 'Saint Thomas' }, { code: 'JM-07', countryCode: 'JM', name: 'Trelawny' }, { code: 'JM-10', countryCode: 'JM', name: 'Westmoreland' }, { code: 'JO-AQ', countryCode: 'JO', name: "Al 'Aqabah" }, { code: 'JO-AM', countryCode: 'JO', name: "Al 'Asimah" }, { code: 'JO-BA', countryCode: 'JO', name: "Al Balqa'" }, { code: 'JO-KA', countryCode: 'JO', name: 'Al Karak' }, { code: 'JO-MA', countryCode: 'JO', name: 'Al Mafraq' }, { code: 'JO-AT', countryCode: 'JO', name: 'At Tafilah' }, { code: 'JO-AZ', countryCode: 'JO', name: "Az Zarqa'" }, { code: 'JO-IR', countryCode: 'JO', name: 'Irbid' }, { code: 'JO-MN', countryCode: 'JO', name: "Ma'an" }, { code: 'JO-MD', countryCode: 'JO', name: 'Madaba' }, { code: 'JP-23', countryCode: 'JP', name: 'Aichi' }, { code: 'JP-05', countryCode: 'JP', name: 'Akita' }, { code: 'JP-02', countryCode: 'JP', name: 'Aomori' }, { code: 'JP-12', countryCode: 'JP', name: 'Chiba' }, { code: 'JP-38', countryCode: 'JP', name: 'Ehime' }, { code: 'JP-18', countryCode: 'JP', name: 'Fukui' }, { code: 'JP-40', countryCode: 'JP', name: 'Fukuoka' }, { code: 'JP-07', countryCode: 'JP', name: 'Fukushima' }, { code: 'JP-21', countryCode: 'JP', name: 'Gifu' }, { code: 'JP-10', countryCode: 'JP', name: 'Gunma' }, { code: 'JP-34', countryCode: 'JP', name: 'Hiroshima' }, { code: 'JP-01', countryCode: 'JP', name: 'Hokkaido' }, { code: 'JP-28', countryCode: 'JP', name: 'Hyogo' }, { code: 'JP-08', countryCode: 'JP', name: 'Ibaraki' }, { code: 'JP-17', countryCode: 'JP', name: 'Ishikawa' }, { code: 'JP-03', countryCode: 'JP', name: 'Iwate' }, { code: 'JP-37', countryCode: 'JP', name: 'Kagawa' }, { code: 'JP-46', countryCode: 'JP', name: 'Kagoshima' }, { code: 'JP-14', countryCode: 'JP', name: 'Kanagawa' }, { code: 'JP-39', countryCode: 'JP', name: 'Kochi' }, { code: 'JP-43', countryCode: 'JP', name: 'Kumamoto' }, { code: 'JP-26', countryCode: 'JP', name: 'Kyoto' }, { code: 'JP-24', countryCode: 'JP', name: 'Mie' }, { code: 'JP-04', countryCode: 'JP', name: 'Miyagi' }, { code: 'JP-45', countryCode: 'JP', name: 'Miyazaki' }, { code: 'JP-20', countryCode: 'JP', name: 'Nagano' }, { code: 'JP-42', countryCode: 'JP', name: 'Nagasaki' }, { code: 'JP-29', countryCode: 'JP', name: 'Nara' }, { code: 'JP-15', countryCode: 'JP', name: 'Niigata' }, { code: 'JP-44', countryCode: 'JP', name: 'Oita' }, { code: 'JP-33', countryCode: 'JP', name: 'Okayama' }, { code: 'JP-47', countryCode: 'JP', name: 'Okinawa' }, { code: 'JP-27', countryCode: 'JP', name: 'Osaka' }, { code: 'JP-41', countryCode: 'JP', name: 'Saga' }, { code: 'JP-11', countryCode: 'JP', name: 'Saitama' }, { code: 'JP-25', countryCode: 'JP', name: 'Shiga' }, { code: 'JP-32', countryCode: 'JP', name: 'Shimane' }, { code: 'JP-22', countryCode: 'JP', name: 'Shizuoka' }, { code: 'JP-09', countryCode: 'JP', name: 'Tochigi' }, { code: 'JP-36', countryCode: 'JP', name: 'Tokushima' }, { code: 'JP-13', countryCode: 'JP', name: 'Tokyo' }, { code: 'JP-31', countryCode: 'JP', name: 'Tottori' }, { code: 'JP-16', countryCode: 'JP', name: 'Toyama' }, { code: 'JP-30', countryCode: 'JP', name: 'Wakayama' }, { code: 'JP-06', countryCode: 'JP', name: 'Yamagata' }, { code: 'JP-35', countryCode: 'JP', name: 'Yamaguchi' }, { code: 'JP-19', countryCode: 'JP', name: 'Yamanashi' }, { code: 'KE-01', countryCode: 'KE', name: 'Baringo' }, { code: 'KE-02', countryCode: 'KE', name: 'Bomet' }, { code: 'KE-03', countryCode: 'KE', name: 'Bungoma' }, { code: 'KE-04', countryCode: 'KE', name: 'Busia' }, { code: 'KE-06', countryCode: 'KE', name: 'Embu' }, { code: 'KE-07', countryCode: 'KE', name: 'Garissa' }, { code: 'KE-08', countryCode: 'KE', name: 'Homa Bay' }, { code: 'KE-09', countryCode: 'KE', name: 'Isiolo' }, { code: 'KE-10', countryCode: 'KE', name: 'Kajiado' }, { code: 'KE-11', countryCode: 'KE', name: 'Kakamega' }, { code: 'KE-12', countryCode: 'KE', name: 'Kericho' }, { code: 'KE-13', countryCode: 'KE', name: 'Kiambu' }, { code: 'KE-14', countryCode: 'KE', name: 'Kilifi' }, { code: 'KE-15', countryCode: 'KE', name: 'Kirinyaga' }, { code: 'KE-16', countryCode: 'KE', name: 'Kisii' }, { code: 'KE-17', countryCode: 'KE', name: 'Kisumu' }, { code: 'KE-18', countryCode: 'KE', name: 'Kitui' }, { code: 'KE-19', countryCode: 'KE', name: 'Kwale' }, { code: 'KE-20', countryCode: 'KE', name: 'Laikipia' }, { code: 'KE-21', countryCode: 'KE', name: 'Lamu' }, { code: 'KE-22', countryCode: 'KE', name: 'Machakos' }, { code: 'KE-23', countryCode: 'KE', name: 'Makueni' }, { code: 'KE-24', countryCode: 'KE', name: 'Mandera' }, { code: 'KE-25', countryCode: 'KE', name: 'Marsabit' }, { code: 'KE-26', countryCode: 'KE', name: 'Meru' }, { code: 'KE-27', countryCode: 'KE', name: 'Migori' }, { code: 'KE-28', countryCode: 'KE', name: 'Mombasa' }, { code: 'KE-29', countryCode: 'KE', name: "Murang'a" }, { code: 'KE-30', countryCode: 'KE', name: 'Nairobi City' }, { code: 'KE-31', countryCode: 'KE', name: 'Nakuru' }, { code: 'KE-32', countryCode: 'KE', name: 'Nandi' }, { code: 'KE-33', countryCode: 'KE', name: 'Narok' }, { code: 'KE-34', countryCode: 'KE', name: 'Nyamira' }, { code: 'KE-36', countryCode: 'KE', name: 'Nyeri' }, { code: 'KE-37', countryCode: 'KE', name: 'Samburu' }, { code: 'KE-38', countryCode: 'KE', name: 'Siaya' }, { code: 'KE-39', countryCode: 'KE', name: 'Taita/Taveta' }, { code: 'KE-40', countryCode: 'KE', name: 'Tana River' }, { code: 'KE-41', countryCode: 'KE', name: 'Tharaka-Nithi' }, { code: 'KE-42', countryCode: 'KE', name: 'Trans Nzoia' }, { code: 'KE-43', countryCode: 'KE', name: 'Turkana' }, { code: 'KE-44', countryCode: 'KE', name: 'Uasin Gishu' }, { code: 'KE-45', countryCode: 'KE', name: 'Vihiga' }, { code: 'KE-46', countryCode: 'KE', name: 'Wajir' }, { code: 'KE-47', countryCode: 'KE', name: 'West Pokot' }, { code: 'KG-B', countryCode: 'KG', name: 'Batken' }, { code: 'KG-GB', countryCode: 'KG', name: 'Bishkek' }, { code: 'KG-C', countryCode: 'KG', name: 'Chuy' }, { code: 'KG-J', countryCode: 'KG', name: 'Jalal-Abad' }, { code: 'KG-N', countryCode: 'KG', name: 'Naryn' }, { code: 'KG-GO', countryCode: 'KG', name: 'Osh' }, { code: 'KG-T', countryCode: 'KG', name: 'Talas' }, { code: 'KG-Y', countryCode: 'KG', name: 'Ysyk-Kol' }, { code: 'KH-2', countryCode: 'KH', name: 'Baat Dambang' }, { code: 'KH-1', countryCode: 'KH', name: 'Banteay Mean Chey' }, { code: 'KH-3', countryCode: 'KH', name: 'Kampong Chaam' }, { code: 'KH-4', countryCode: 'KH', name: 'Kampong Chhnang' }, { code: 'KH-5', countryCode: 'KH', name: 'Kampong Spueu' }, { code: 'KH-6', countryCode: 'KH', name: 'Kampong Thum' }, { code: 'KH-7', countryCode: 'KH', name: 'Kampot' }, { code: 'KH-8', countryCode: 'KH', name: 'Kandaal' }, { code: 'KH-9', countryCode: 'KH', name: 'Kaoh Kong' }, { code: 'KH-10', countryCode: 'KH', name: 'Kracheh' }, { code: 'KH-23', countryCode: 'KH', name: 'Krong Kaeb' }, { code: 'KH-24', countryCode: 'KH', name: 'Krong Pailin' }, { code: 'KH-18', countryCode: 'KH', name: 'Krong Preah Sihanouk' }, { code: 'KH-11', countryCode: 'KH', name: 'Mondol Kiri' }, { code: 'KH-22', countryCode: 'KH', name: 'Otdar Mean Chey' }, { code: 'KH-12', countryCode: 'KH', name: 'Phnom Penh' }, { code: 'KH-15', countryCode: 'KH', name: 'Pousaat' }, { code: 'KH-13', countryCode: 'KH', name: 'Preah Vihear' }, { code: 'KH-14', countryCode: 'KH', name: 'Prey Veaeng' }, { code: 'KH-16', countryCode: 'KH', name: 'Rotanak Kiri' }, { code: 'KH-17', countryCode: 'KH', name: 'Siem Reab' }, { code: 'KH-19', countryCode: 'KH', name: 'Stueng Traeng' }, { code: 'KH-20', countryCode: 'KH', name: 'Svaay Rieng' }, { code: 'KH-21', countryCode: 'KH', name: 'Taakaev' }, { code: 'KI-G', countryCode: 'KI', name: 'Gilbert Islands' }, { code: 'KI-L', countryCode: 'KI', name: 'Line Islands' }, { code: 'KM-A', countryCode: 'KM', name: 'Anjouan' }, { code: 'KM-G', countryCode: 'KM', name: 'Grande Comore' }, { code: 'KM-M', countryCode: 'KM', name: 'Moheli' }, { code: 'KN-03', countryCode: 'KN', name: 'Saint George Basseterre' }, { code: 'KN-10', countryCode: 'KN', name: 'Saint Paul Charlestown' }, { code: 'KP-04', countryCode: 'KP', name: 'Chagang-do' }, { code: 'KP-09', countryCode: 'KP', name: 'Hamgyong-bukto' }, { code: 'KP-08', countryCode: 'KP', name: 'Hamgyong-namdo' }, { code: 'KP-06', countryCode: 'KP', name: 'Hwanghae-bukto' }, { code: 'KP-05', countryCode: 'KP', name: 'Hwanghae-namdo' }, { code: 'KP-07', countryCode: 'KP', name: 'Kangwon-do' }, { code: 'KP-13', countryCode: 'KP', name: 'Nason' }, { code: 'KP-03', countryCode: 'KP', name: "P'yongan-bukto" }, { code: 'KP-02', countryCode: 'KP', name: "P'yongan-namdo" }, { code: 'KP-01', countryCode: 'KP', name: "P'yongyang" }, { code: 'KP-10', countryCode: 'KP', name: 'Yanggang-do' }, { code: 'KR-26', countryCode: 'KR', name: 'Busan-gwangyeoksi' }, { code: 'KR-43', countryCode: 'KR', name: 'Chungcheongbuk-do' }, { code: 'KR-44', countryCode: 'KR', name: 'Chungcheongnam-do' }, { code: 'KR-27', countryCode: 'KR', name: 'Daegu-gwangyeoksi' }, { code: 'KR-30', countryCode: 'KR', name: 'Daejeon-gwangyeoksi' }, { code: 'KR-42', countryCode: 'KR', name: 'Gangwon-do' }, { code: 'KR-29', countryCode: 'KR', name: 'Gwangju-gwangyeoksi' }, { code: 'KR-41', countryCode: 'KR', name: 'Gyeonggi-do' }, { code: 'KR-47', countryCode: 'KR', name: 'Gyeongsangbuk-do' }, { code: 'KR-48', countryCode: 'KR', name: 'Gyeongsangnam-do' }, { code: 'KR-28', countryCode: 'KR', name: 'Incheon-gwangyeoksi' }, { code: 'KR-49', countryCode: 'KR', name: 'Jeju-teukbyeoljachido' }, { code: 'KR-45', countryCode: 'KR', name: 'Jeollabuk-do' }, { code: 'KR-46', countryCode: 'KR', name: 'Jeollanam-do' }, { code: 'KR-11', countryCode: 'KR', name: 'Seoul-teukbyeolsi' }, { code: 'KR-31', countryCode: 'KR', name: 'Ulsan-gwangyeoksi' }, { code: 'KW-KU', countryCode: 'KW', name: "Al 'Asimah" }, { code: 'KW-AH', countryCode: 'KW', name: 'Al Ahmadi' }, { code: 'KW-FA', countryCode: 'KW', name: 'Al Farwaniyah' }, { code: 'KW-JA', countryCode: 'KW', name: 'Al Jahra' }, { code: 'KW-HA', countryCode: 'KW', name: 'Hawalli' }, { code: 'KW-MU', countryCode: 'KW', name: 'Mubarak al Kabir' }, { code: 'KZ-ALA', countryCode: 'KZ', name: 'Almaty' }, { code: 'KZ-ALM', countryCode: 'KZ', name: 'Almaty oblysy' }, { code: 'KZ-AKM', countryCode: 'KZ', name: 'Aqmola oblysy' }, { code: 'KZ-AKT', countryCode: 'KZ', name: 'Aqtobe oblysy' }, { code: 'KZ-AST', countryCode: 'KZ', name: 'Astana' }, { code: 'KZ-ATY', countryCode: 'KZ', name: 'Atyrau oblysy' }, { code: 'KZ-ZAP', countryCode: 'KZ', name: 'Batys Qazaqstan oblysy' }, { code: 'KZ-BAY', countryCode: 'KZ', name: 'Bayqongyr' }, { code: 'KZ-MAN', countryCode: 'KZ', name: 'Mangghystau oblysy' }, { code: 'KZ-YUZ', countryCode: 'KZ', name: 'Ongtustik Qazaqstan oblysy' }, { code: 'KZ-PAV', countryCode: 'KZ', name: 'Pavlodar oblysy' }, { code: 'KZ-KAR', countryCode: 'KZ', name: 'Qaraghandy oblysy' }, { code: 'KZ-KUS', countryCode: 'KZ', name: 'Qostanay oblysy' }, { code: 'KZ-KZY', countryCode: 'KZ', name: 'Qyzylorda oblysy' }, { code: 'KZ-VOS', countryCode: 'KZ', name: 'Shyghys Qazaqstan oblysy' }, { code: 'KZ-SEV', countryCode: 'KZ', name: 'Soltustik Qazaqstan oblysy' }, { code: 'KZ-ZHA', countryCode: 'KZ', name: 'Zhambyl oblysy' }, { code: 'LA-AT', countryCode: 'LA', name: 'Attapu' }, { code: 'LA-BK', countryCode: 'LA', name: 'Bokeo' }, { code: 'LA-BL', countryCode: 'LA', name: 'Bolikhamxai' }, { code: 'LA-CH', countryCode: 'LA', name: 'Champasak' }, { code: 'LA-HO', countryCode: 'LA', name: 'Houaphan' }, { code: 'LA-KH', countryCode: 'LA', name: 'Khammouan' }, { code: 'LA-LM', countryCode: 'LA', name: 'Louang Namtha' }, { code: 'LA-LP', countryCode: 'LA', name: 'Louangphabang' }, { code: 'LA-OU', countryCode: 'LA', name: 'Oudomxai' }, { code: 'LA-PH', countryCode: 'LA', name: 'Phongsali' }, { code: 'LA-SL', countryCode: 'LA', name: 'Salavan' }, { code: 'LA-SV', countryCode: 'LA', name: 'Savannakhet' }, { code: 'LA-VI', countryCode: 'LA', name: 'Viangchan' }, { code: 'LA-XA', countryCode: 'LA', name: 'Xaignabouli' }, { code: 'LA-XE', countryCode: 'LA', name: 'Xekong' }, { code: 'LA-XI', countryCode: 'LA', name: 'Xiangkhouang' }, { code: 'LB-AK', countryCode: 'LB', name: 'Aakkar' }, { code: 'LB-BH', countryCode: 'LB', name: 'Baalbek-Hermel' }, { code: 'LB-BI', countryCode: 'LB', name: 'Beqaa' }, { code: 'LB-BA', countryCode: 'LB', name: 'Beyrouth' }, { code: 'LB-AS', countryCode: 'LB', name: 'Liban-Nord' }, { code: 'LB-JA', countryCode: 'LB', name: 'Liban-Sud' }, { code: 'LB-JL', countryCode: 'LB', name: 'Mont-Liban' }, { code: 'LB-NA', countryCode: 'LB', name: 'Nabatiye' }, { code: 'LC-01', countryCode: 'LC', name: 'Anse la Raye' }, { code: 'LC-02', countryCode: 'LC', name: 'Castries' }, { code: 'LC-05', countryCode: 'LC', name: 'Dennery' }, { code: 'LC-06', countryCode: 'LC', name: 'Gros Islet' }, { code: 'LC-07', countryCode: 'LC', name: 'Laborie' }, { code: 'LC-08', countryCode: 'LC', name: 'Micoud' }, { code: 'LC-10', countryCode: 'LC', name: 'Soufriere' }, { code: 'LC-11', countryCode: 'LC', name: 'Vieux Fort' }, { code: 'LI-01', countryCode: 'LI', name: 'Balzers' }, { code: 'LI-02', countryCode: 'LI', name: 'Eschen' }, { code: 'LI-03', countryCode: 'LI', name: 'Gamprin' }, { code: 'LI-04', countryCode: 'LI', name: 'Mauren' }, { code: 'LI-05', countryCode: 'LI', name: 'Planken' }, { code: 'LI-06', countryCode: 'LI', name: 'Ruggell' }, { code: 'LI-07', countryCode: 'LI', name: 'Schaan' }, { code: 'LI-08', countryCode: 'LI', name: 'Schellenberg' }, { code: 'LI-09', countryCode: 'LI', name: 'Triesen' }, { code: 'LI-10', countryCode: 'LI', name: 'Triesenberg' }, { code: 'LI-11', countryCode: 'LI', name: 'Vaduz' }, { code: 'LK-2', countryCode: 'LK', name: 'Central Province' }, { code: 'LK-5', countryCode: 'LK', name: 'Eastern Province' }, { code: 'LK-7', countryCode: 'LK', name: 'North Central Province' }, { code: 'LK-6', countryCode: 'LK', name: 'North Western Province' }, { code: 'LK-4', countryCode: 'LK', name: 'Northern Province' }, { code: 'LK-9', countryCode: 'LK', name: 'Sabaragamuwa Province' }, { code: 'LK-3', countryCode: 'LK', name: 'Southern Province' }, { code: 'LK-8', countryCode: 'LK', name: 'Uva Province' }, { code: 'LK-1', countryCode: 'LK', name: 'Western Province' }, { code: 'LR-BM', countryCode: 'LR', name: 'Bomi' }, { code: 'LR-BG', countryCode: 'LR', name: 'Bong' }, { code: 'LR-GP', countryCode: 'LR', name: 'Gbarpolu' }, { code: 'LR-GB', countryCode: 'LR', name: 'Grand Bassa' }, { code: 'LR-CM', countryCode: 'LR', name: 'Grand Cape Mount' }, { code: 'LR-GG', countryCode: 'LR', name: 'Grand Gedeh' }, { code: 'LR-GK', countryCode: 'LR', name: 'Grand Kru' }, { code: 'LR-LO', countryCode: 'LR', name: 'Lofa' }, { code: 'LR-MG', countryCode: 'LR', name: 'Margibi' }, { code: 'LR-MY', countryCode: 'LR', name: 'Maryland' }, { code: 'LR-MO', countryCode: 'LR', name: 'Montserrado' }, { code: 'LR-NI', countryCode: 'LR', name: 'Nimba' }, { code: 'LR-RI', countryCode: 'LR', name: 'River Cess' }, { code: 'LR-RG', countryCode: 'LR', name: 'River Gee' }, { code: 'LR-SI', countryCode: 'LR', name: 'Sinoe' }, { code: 'LS-D', countryCode: 'LS', name: 'Berea' }, { code: 'LS-B', countryCode: 'LS', name: 'Butha-Buthe' }, { code: 'LS-C', countryCode: 'LS', name: 'Leribe' }, { code: 'LS-E', countryCode: 'LS', name: 'Mafeteng' }, { code: 'LS-A', countryCode: 'LS', name: 'Maseru' }, { code: 'LS-F', countryCode: 'LS', name: "Mohale's Hoek" }, { code: 'LS-J', countryCode: 'LS', name: 'Mokhotlong' }, { code: 'LS-H', countryCode: 'LS', name: "Qacha's Nek" }, { code: 'LS-G', countryCode: 'LS', name: 'Quthing' }, { code: 'LS-K', countryCode: 'LS', name: 'Thaba-Tseka' }, { code: 'LT-AL', countryCode: 'LT', name: 'Alytaus apskritis' }, { code: 'LT-KU', countryCode: 'LT', name: 'Kauno apskritis' }, { code: 'LT-KL', countryCode: 'LT', name: 'Klaipedos apskritis' }, { code: 'LT-MR', countryCode: 'LT', name: 'Marijampoles apskritis' }, { code: 'LT-PN', countryCode: 'LT', name: 'Panevezio apskritis' }, { code: 'LT-SA', countryCode: 'LT', name: 'Siauliu apskritis' }, { code: 'LT-TA', countryCode: 'LT', name: 'Taurages apskritis' }, { code: 'LT-TE', countryCode: 'LT', name: 'Telsiu apskritis' }, { code: 'LT-UT', countryCode: 'LT', name: 'Utenos apskritis' }, { code: 'LT-VL', countryCode: 'LT', name: 'Vilniaus apskritis' }, { code: 'LU-DI', countryCode: 'LU', name: 'Diekirch' }, { code: 'LU-GR', countryCode: 'LU', name: 'Grevenmacher' }, { code: 'LU-LU', countryCode: 'LU', name: 'Luxembourg' }, { code: 'LV-011', countryCode: 'LV', name: 'Adazu novads' }, { code: 'LV-001', countryCode: 'LV', name: 'Aglonas novads' }, { code: 'LV-002', countryCode: 'LV', name: 'Aizkraukles novads' }, { code: 'LV-003', countryCode: 'LV', name: 'Aizputes novads' }, { code: 'LV-005', countryCode: 'LV', name: 'Alojas novads' }, { code: 'LV-007', countryCode: 'LV', name: 'Aluksnes novads' }, { code: 'LV-012', countryCode: 'LV', name: 'Babites novads' }, { code: 'LV-014', countryCode: 'LV', name: 'Baltinavas novads' }, { code: 'LV-015', countryCode: 'LV', name: 'Balvu novads' }, { code: 'LV-016', countryCode: 'LV', name: 'Bauskas novads' }, { code: 'LV-017', countryCode: 'LV', name: 'Beverinas novads' }, { code: 'LV-018', countryCode: 'LV', name: 'Brocenu novads' }, { code: 'LV-020', countryCode: 'LV', name: 'Carnikavas novads' }, { code: 'LV-022', countryCode: 'LV', name: 'Cesu novads' }, { code: 'LV-021', countryCode: 'LV', name: 'Cesvaines novads' }, { code: 'LV-023', countryCode: 'LV', name: 'Ciblas novads' }, { code: 'LV-025', countryCode: 'LV', name: 'Daugavpils novads' }, { code: 'LV-026', countryCode: 'LV', name: 'Dobeles novads' }, { code: 'LV-027', countryCode: 'LV', name: 'Dundagas novads' }, { code: 'LV-033', countryCode: 'LV', name: 'Gulbenes novads' }, { code: 'LV-034', countryCode: 'LV', name: 'Iecavas novads' }, { code: 'LV-037', countryCode: 'LV', name: 'Incukalna novads' }, { code: 'LV-038', countryCode: 'LV', name: 'Jaunjelgavas novads' }, { code: 'LV-039', countryCode: 'LV', name: 'Jaunpiebalgas novads' }, { code: 'LV-040', countryCode: 'LV', name: 'Jaunpils novads' }, { code: 'LV-042', countryCode: 'LV', name: 'Jekabpils novads' }, { code: 'LV-JEL', countryCode: 'LV', name: 'Jelgava' }, { code: 'LV-041', countryCode: 'LV', name: 'Jelgavas novads' }, { code: 'LV-JUR', countryCode: 'LV', name: 'Jurmala' }, { code: 'LV-052', countryCode: 'LV', name: 'Kekavas novads' }, { code: 'LV-046', countryCode: 'LV', name: 'Kokneses novads' }, { code: 'LV-047', countryCode: 'LV', name: 'Kraslavas novads' }, { code: 'LV-050', countryCode: 'LV', name: 'Kuldigas novads' }, { code: 'LV-LPX', countryCode: 'LV', name: 'Liepaja' }, { code: 'LV-054', countryCode: 'LV', name: 'Limbazu novads' }, { code: 'LV-057', countryCode: 'LV', name: 'Lubanas novads' }, { code: 'LV-058', countryCode: 'LV', name: 'Ludzas novads' }, { code: 'LV-059', countryCode: 'LV', name: 'Madonas novads' }, { code: 'LV-061', countryCode: 'LV', name: 'Malpils novads' }, { code: 'LV-067', countryCode: 'LV', name: 'Ogres novads' }, { code: 'LV-068', countryCode: 'LV', name: 'Olaines novads' }, { code: 'LV-069', countryCode: 'LV', name: 'Ozolnieku novads' }, { code: 'LV-073', countryCode: 'LV', name: 'Preilu novads' }, { code: 'LV-077', countryCode: 'LV', name: 'Rezeknes novads' }, { code: 'LV-RIX', countryCode: 'LV', name: 'Riga' }, { code: 'LV-079', countryCode: 'LV', name: 'Rojas novads' }, { code: 'LV-080', countryCode: 'LV', name: 'Ropazu novads' }, { code: 'LV-082', countryCode: 'LV', name: 'Rugaju novads' }, { code: 'LV-083', countryCode: 'LV', name: 'Rundales novads' }, { code: 'LV-086', countryCode: 'LV', name: 'Salacgrivas novads' }, { code: 'LV-088', countryCode: 'LV', name: 'Saldus novads' }, { code: 'LV-090', countryCode: 'LV', name: 'Sejas novads' }, { code: 'LV-091', countryCode: 'LV', name: 'Siguldas novads' }, { code: 'LV-093', countryCode: 'LV', name: 'Skrundas novads' }, { code: 'LV-095', countryCode: 'LV', name: 'Stopinu novads' }, { code: 'LV-096', countryCode: 'LV', name: 'Strencu novads' }, { code: 'LV-097', countryCode: 'LV', name: 'Talsu novads' }, { code: 'LV-099', countryCode: 'LV', name: 'Tukuma novads' }, { code: 'LV-100', countryCode: 'LV', name: 'Vainodes novads' }, { code: 'LV-101', countryCode: 'LV', name: 'Valkas novads' }, { code: 'LV-VMR', countryCode: 'LV', name: 'Valmiera' }, { code: 'LV-103', countryCode: 'LV', name: 'Varkavas novads' }, { code: 'LV-105', countryCode: 'LV', name: 'Vecumnieku novads' }, { code: 'LV-106', countryCode: 'LV', name: 'Ventspils novads' }, { code: 'LY-BU', countryCode: 'LY', name: 'Al Butnan' }, { code: 'LY-JA', countryCode: 'LY', name: 'Al Jabal al Akhdar' }, { code: 'LY-JG', countryCode: 'LY', name: 'Al Jabal al Gharbi' }, { code: 'LY-JI', countryCode: 'LY', name: 'Al Jafarah' }, { code: 'LY-JU', countryCode: 'LY', name: 'Al Jufrah' }, { code: 'LY-KF', countryCode: 'LY', name: 'Al Kufrah' }, { code: 'LY-MJ', countryCode: 'LY', name: 'Al Marj' }, { code: 'LY-MB', countryCode: 'LY', name: 'Al Marqab' }, { code: 'LY-WA', countryCode: 'LY', name: 'Al Wahat' }, { code: 'LY-NQ', countryCode: 'LY', name: 'An Nuqat al Khams' }, { code: 'LY-ZA', countryCode: 'LY', name: 'Az Zawiyah' }, { code: 'LY-BA', countryCode: 'LY', name: 'Banghazi' }, { code: 'LY-DR', countryCode: 'LY', name: 'Darnah' }, { code: 'LY-GT', countryCode: 'LY', name: 'Ghat' }, { code: 'LY-MI', countryCode: 'LY', name: 'Misratah' }, { code: 'LY-MQ', countryCode: 'LY', name: 'Murzuq' }, { code: 'LY-NL', countryCode: 'LY', name: 'Nalut' }, { code: 'LY-SB', countryCode: 'LY', name: 'Sabha' }, { code: 'LY-SR', countryCode: 'LY', name: 'Surt' }, { code: 'LY-TB', countryCode: 'LY', name: 'Tarabulus' }, { code: 'LY-WD', countryCode: 'LY', name: 'Wadi al Hayat' }, { code: 'LY-WS', countryCode: 'LY', name: "Wadi ash Shati'" }, { code: 'MA-09', countryCode: 'MA', name: 'Chaouia-Ouardigha' }, { code: 'MA-10', countryCode: 'MA', name: 'Doukhala-Abda' }, { code: 'MA-05', countryCode: 'MA', name: 'Fes-Boulemane' }, { code: 'MA-02', countryCode: 'MA', name: 'Gharb-Chrarda-Beni Hssen' }, { code: 'MA-08', countryCode: 'MA', name: 'Grand Casablanca' }, { code: 'MA-14', countryCode: 'MA', name: 'Guelmim-Es Semara' }, { code: 'MA-04', countryCode: 'MA', name: "L'Oriental" }, { code: 'MA-11', countryCode: 'MA', name: 'Marrakech-Tensift-Al Haouz' }, { code: 'MA-06', countryCode: 'MA', name: 'Meknes-Tafilalet' }, { code: 'MA-07', countryCode: 'MA', name: 'Rabat-Sale-Zemmour-Zaer' }, { code: 'MA-13', countryCode: 'MA', name: 'Souss-Massa-Draa' }, { code: 'MA-12', countryCode: 'MA', name: 'Tadla-Azilal' }, { code: 'MA-01', countryCode: 'MA', name: 'Tanger-Tetouan' }, { code: 'MA-03', countryCode: 'MA', name: 'Taza-Al Hoceima-Taounate' }, { code: 'MC-FO', countryCode: 'MC', name: 'Fontvieille' }, { code: 'MC-CO', countryCode: 'MC', name: 'La Condamine' }, { code: 'MC-MO', countryCode: 'MC', name: 'Monaco-Ville' }, { code: 'MC-MG', countryCode: 'MC', name: 'Moneghetti' }, { code: 'MC-MC', countryCode: 'MC', name: 'Monte-Carlo' }, { code: 'MC-SR', countryCode: 'MC', name: 'Saint-Roman' }, { code: 'MD-AN', countryCode: 'MD', name: 'Anenii Noi' }, { code: 'MD-BA', countryCode: 'MD', name: 'Balti' }, { code: 'MD-BS', countryCode: 'MD', name: 'Basarabeasca' }, { code: 'MD-BD', countryCode: 'MD', name: 'Bender' }, { code: 'MD-BR', countryCode: 'MD', name: 'Briceni' }, { code: 'MD-CA', countryCode: 'MD', name: 'Cahul' }, { code: 'MD-CL', countryCode: 'MD', name: 'Calarasi' }, { code: 'MD-CT', countryCode: 'MD', name: 'Cantemir' }, { code: 'MD-CS', countryCode: 'MD', name: 'Causeni' }, { code: 'MD-CU', countryCode: 'MD', name: 'Chisinau' }, { code: 'MD-CM', countryCode: 'MD', name: 'Cimislia' }, { code: 'MD-CR', countryCode: 'MD', name: 'Criuleni' }, { code: 'MD-DO', countryCode: 'MD', name: 'Donduseni' }, { code: 'MD-DR', countryCode: 'MD', name: 'Drochia' }, { code: 'MD-DU', countryCode: 'MD', name: 'Dubasari' }, { code: 'MD-ED', countryCode: 'MD', name: 'Edinet' }, { code: 'MD-FA', countryCode: 'MD', name: 'Falesti' }, { code: 'MD-FL', countryCode: 'MD', name: 'Floresti' }, { code: 'MD-GA', countryCode: 'MD', name: 'Gagauzia, Unitatea teritoriala autonoma' }, { code: 'MD-GL', countryCode: 'MD', name: 'Glodeni' }, { code: 'MD-HI', countryCode: 'MD', name: 'Hincesti' }, { code: 'MD-IA', countryCode: 'MD', name: 'Ialoveni' }, { code: 'MD-LE', countryCode: 'MD', name: 'Leova' }, { code: 'MD-NI', countryCode: 'MD', name: 'Nisporeni' }, { code: 'MD-OC', countryCode: 'MD', name: 'Ocnita' }, { code: 'MD-OR', countryCode: 'MD', name: 'Orhei' }, { code: 'MD-RE', countryCode: 'MD', name: 'Rezina' }, { code: 'MD-RI', countryCode: 'MD', name: 'Riscani' }, { code: 'MD-SI', countryCode: 'MD', name: 'Singerei' }, { code: 'MD-SD', countryCode: 'MD', name: 'Soldanesti' }, { code: 'MD-SO', countryCode: 'MD', name: 'Soroca' }, { code: 'MD-SV', countryCode: 'MD', name: 'Stefan Voda' }, { code: 'MD-SN', countryCode: 'MD', name: 'Stinga Nistrului, unitatea teritoriala din' }, { code: 'MD-ST', countryCode: 'MD', name: 'Straseni' }, { code: 'MD-TA', countryCode: 'MD', name: 'Taraclia' }, { code: 'MD-TE', countryCode: 'MD', name: 'Telenesti' }, { code: 'MD-UN', countryCode: 'MD', name: 'Ungheni' }, { code: 'ME-02', countryCode: 'ME', name: 'Bar' }, { code: 'ME-05', countryCode: 'ME', name: 'Budva' }, { code: 'ME-06', countryCode: 'ME', name: 'Cetinje' }, { code: 'ME-07', countryCode: 'ME', name: 'Danilovgrad' }, { code: 'ME-08', countryCode: 'ME', name: 'Herceg-Novi' }, { code: 'ME-09', countryCode: 'ME', name: 'Kolasin' }, { code: 'ME-10', countryCode: 'ME', name: 'Kotor' }, { code: 'ME-11', countryCode: 'ME', name: 'Mojkovac' }, { code: 'ME-12', countryCode: 'ME', name: 'Niksic' }, { code: 'ME-16', countryCode: 'ME', name: 'Podgorica' }, { code: 'ME-19', countryCode: 'ME', name: 'Tivat' }, { code: 'ME-20', countryCode: 'ME', name: 'Ulcinj' }, { code: 'ME-21', countryCode: 'ME', name: 'Zabljak' }, { code: 'MG-T', countryCode: 'MG', name: 'Antananarivo' }, { code: 'MG-D', countryCode: 'MG', name: 'Antsiranana' }, { code: 'MG-F', countryCode: 'MG', name: 'Fianarantsoa' }, { code: 'MG-M', countryCode: 'MG', name: 'Mahajanga' }, { code: 'MG-A', countryCode: 'MG', name: 'Toamasina' }, { code: 'MG-U', countryCode: 'MG', name: 'Toliara' }, { code: 'MH-ALL', countryCode: 'MH', name: 'Ailinglaplap' }, { code: 'MH-ALK', countryCode: 'MH', name: 'Ailuk' }, { code: 'MH-ARN', countryCode: 'MH', name: 'Arno' }, { code: 'MH-AUR', countryCode: 'MH', name: 'Aur' }, { code: 'MH-KIL', countryCode: 'MH', name: 'Bikini and Kili' }, { code: 'MH-EBO', countryCode: 'MH', name: 'Ebon' }, { code: 'MH-ENI', countryCode: 'MH', name: 'Enewetak and Ujelang' }, { code: 'MH-JAB', countryCode: 'MH', name: 'Jabat' }, { code: 'MH-JAL', countryCode: 'MH', name: 'Jaluit' }, { code: 'MH-KWA', countryCode: 'MH', name: 'Kwajalein' }, { code: 'MH-LAE', countryCode: 'MH', name: 'Lae' }, { code: 'MH-LIB', countryCode: 'MH', name: 'Lib' }, { code: 'MH-LIK', countryCode: 'MH', name: 'Likiep' }, { code: 'MH-MAJ', countryCode: 'MH', name: 'Majuro' }, { code: 'MH-MAL', countryCode: 'MH', name: 'Maloelap' }, { code: 'MH-MEJ', countryCode: 'MH', name: 'Mejit' }, { code: 'MH-MIL', countryCode: 'MH', name: 'Mili' }, { code: 'MH-NMK', countryCode: 'MH', name: 'Namdrik' }, { code: 'MH-NMU', countryCode: 'MH', name: 'Namu' }, { code: 'MH-RON', countryCode: 'MH', name: 'Rongelap' }, { code: 'MH-UJA', countryCode: 'MH', name: 'Ujae' }, { code: 'MH-UTI', countryCode: 'MH', name: 'Utrik' }, { code: 'MH-WTH', countryCode: 'MH', name: 'Wotho' }, { code: 'MH-WTJ', countryCode: 'MH', name: 'Wotje' }, { code: 'MK-02', countryCode: 'MK', name: 'Aracinovo' }, { code: 'MK-03', countryCode: 'MK', name: 'Berovo' }, { code: 'MK-04', countryCode: 'MK', name: 'Bitola' }, { code: 'MK-05', countryCode: 'MK', name: 'Bogdanci' }, { code: 'MK-06', countryCode: 'MK', name: 'Bogovinje' }, { code: 'MK-07', countryCode: 'MK', name: 'Bosilovo' }, { code: 'MK-08', countryCode: 'MK', name: 'Brvenica' }, { code: 'MK-80', countryCode: 'MK', name: 'Caska' }, { code: 'MK-78', countryCode: 'MK', name: 'Centar Zupa' }, { code: 'MK-81', countryCode: 'MK', name: 'Cesinovo-Oblesevo' }, { code: 'MK-82', countryCode: 'MK', name: 'Cucer Sandevo' }, { code: 'MK-21', countryCode: 'MK', name: 'Debar' }, { code: 'MK-22', countryCode: 'MK', name: 'Debarca' }, { code: 'MK-23', countryCode: 'MK', name: 'Delcevo' }, { code: 'MK-25', countryCode: 'MK', name: 'Demir Hisar' }, { code: 'MK-24', countryCode: 'MK', name: 'Demir Kapija' }, { code: 'MK-26', countryCode: 'MK', name: 'Dojran' }, { code: 'MK-27', countryCode: 'MK', name: 'Dolneni' }, { code: 'MK-18', countryCode: 'MK', name: 'Gevgelija' }, { code: 'MK-19', countryCode: 'MK', name: 'Gostivar' }, { code: 'MK-20', countryCode: 'MK', name: 'Gradsko' }, { code: 'MK-34', countryCode: 'MK', name: 'Ilinden' }, { code: 'MK-35', countryCode: 'MK', name: 'Jegunovce' }, { code: 'MK-37', countryCode: 'MK', name: 'Karbinci' }, { code: 'MK-36', countryCode: 'MK', name: 'Kavadarci' }, { code: 'MK-40', countryCode: 'MK', name: 'Kicevo' }, { code: 'MK-42', countryCode: 'MK', name: 'Kocani' }, { code: 'MK-41', countryCode: 'MK', name: 'Konce' }, { code: 'MK-43', countryCode: 'MK', name: 'Kratovo' }, { code: 'MK-44', countryCode: 'MK', name: 'Kriva Palanka' }, { code: 'MK-45', countryCode: 'MK', name: 'Krivogastani' }, { code: 'MK-46', countryCode: 'MK', name: 'Krusevo' }, { code: 'MK-47', countryCode: 'MK', name: 'Kumanovo' }, { code: 'MK-48', countryCode: 'MK', name: 'Lipkovo' }, { code: 'MK-49', countryCode: 'MK', name: 'Lozovo' }, { code: 'MK-51', countryCode: 'MK', name: 'Makedonska Kamenica' }, { code: 'MK-52', countryCode: 'MK', name: 'Makedonski Brod' }, { code: 'MK-50', countryCode: 'MK', name: 'Mavrovo i Rostusa' }, { code: 'MK-53', countryCode: 'MK', name: 'Mogila' }, { code: 'MK-54', countryCode: 'MK', name: 'Negotino' }, { code: 'MK-55', countryCode: 'MK', name: 'Novaci' }, { code: 'MK-56', countryCode: 'MK', name: 'Novo Selo' }, { code: 'MK-58', countryCode: 'MK', name: 'Ohrid' }, { code: 'MK-60', countryCode: 'MK', name: 'Pehcevo' }, { code: 'MK-59', countryCode: 'MK', name: 'Petrovec' }, { code: 'MK-61', countryCode: 'MK', name: 'Plasnica' }, { code: 'MK-62', countryCode: 'MK', name: 'Prilep' }, { code: 'MK-63', countryCode: 'MK', name: 'Probistip' }, { code: 'MK-64', countryCode: 'MK', name: 'Radovis' }, { code: 'MK-65', countryCode: 'MK', name: 'Rankovce' }, { code: 'MK-66', countryCode: 'MK', name: 'Resen' }, { code: 'MK-67', countryCode: 'MK', name: 'Rosoman' }, { code: 'MK-85', countryCode: 'MK', name: 'Skopje' }, { code: 'MK-70', countryCode: 'MK', name: 'Sopiste' }, { code: 'MK-71', countryCode: 'MK', name: 'Staro Nagoricane' }, { code: 'MK-83', countryCode: 'MK', name: 'Stip' }, { code: 'MK-72', countryCode: 'MK', name: 'Struga' }, { code: 'MK-73', countryCode: 'MK', name: 'Strumica' }, { code: 'MK-74', countryCode: 'MK', name: 'Studenicani' }, { code: 'MK-69', countryCode: 'MK', name: 'Sveti Nikole' }, { code: 'MK-75', countryCode: 'MK', name: 'Tearce' }, { code: 'MK-76', countryCode: 'MK', name: 'Tetovo' }, { code: 'MK-10', countryCode: 'MK', name: 'Valandovo' }, { code: 'MK-11', countryCode: 'MK', name: 'Vasilevo' }, { code: 'MK-13', countryCode: 'MK', name: 'Veles' }, { code: 'MK-12', countryCode: 'MK', name: 'Vevcani' }, { code: 'MK-14', countryCode: 'MK', name: 'Vinica' }, { code: 'MK-16', countryCode: 'MK', name: 'Vrapciste' }, { code: 'MK-32', countryCode: 'MK', name: 'Zelenikovo' }, { code: 'MK-30', countryCode: 'MK', name: 'Zelino' }, { code: 'MK-33', countryCode: 'MK', name: 'Zrnovci' }, { code: 'ML-BKO', countryCode: 'ML', name: 'Bamako' }, { code: 'ML-7', countryCode: 'ML', name: 'Gao' }, { code: 'ML-1', countryCode: 'ML', name: 'Kayes' }, { code: 'ML-8', countryCode: 'ML', name: 'Kidal' }, { code: 'ML-2', countryCode: 'ML', name: 'Koulikoro' }, { code: 'ML-5', countryCode: 'ML', name: 'Mopti' }, { code: 'ML-4', countryCode: 'ML', name: 'Segou' }, { code: 'ML-3', countryCode: 'ML', name: 'Sikasso' }, { code: 'ML-6', countryCode: 'ML', name: 'Tombouctou' }, { code: 'MM-07', countryCode: 'MM', name: 'Ayeyarwady' }, { code: 'MM-02', countryCode: 'MM', name: 'Bago' }, { code: 'MM-14', countryCode: 'MM', name: 'Chin' }, { code: 'MM-11', countryCode: 'MM', name: 'Kachin' }, { code: 'MM-12', countryCode: 'MM', name: 'Kayah' }, { code: 'MM-13', countryCode: 'MM', name: 'Kayin' }, { code: 'MM-03', countryCode: 'MM', name: 'Magway' }, { code: 'MM-04', countryCode: 'MM', name: 'Mandalay' }, { code: 'MM-15', countryCode: 'MM', name: 'Mon' }, { code: 'MM-18', countryCode: 'MM', name: 'Nay Pyi Taw' }, { code: 'MM-16', countryCode: 'MM', name: 'Rakhine' }, { code: 'MM-01', countryCode: 'MM', name: 'Sagaing' }, { code: 'MM-17', countryCode: 'MM', name: 'Shan' }, { code: 'MM-05', countryCode: 'MM', name: 'Tanintharyi' }, { code: 'MM-06', countryCode: 'MM', name: 'Yangon' }, { code: 'MN-073', countryCode: 'MN', name: 'Arhangay' }, { code: 'MN-071', countryCode: 'MN', name: 'Bayan-Olgiy' }, { code: 'MN-069', countryCode: 'MN', name: 'Bayanhongor' }, { code: 'MN-067', countryCode: 'MN', name: 'Bulgan' }, { code: 'MN-037', countryCode: 'MN', name: 'Darhan uul' }, { code: 'MN-061', countryCode: 'MN', name: 'Dornod' }, { code: 'MN-063', countryCode: 'MN', name: 'Dornogovi' }, { code: 'MN-059', countryCode: 'MN', name: 'Dundgovi' }, { code: 'MN-057', countryCode: 'MN', name: 'Dzavhan' }, { code: 'MN-065', countryCode: 'MN', name: 'Govi-Altay' }, { code: 'MN-064', countryCode: 'MN', name: 'Govi-Sumber' }, { code: 'MN-039', countryCode: 'MN', name: 'Hentiy' }, { code: 'MN-043', countryCode: 'MN', name: 'Hovd' }, { code: 'MN-041', countryCode: 'MN', name: 'Hovsgol' }, { code: 'MN-053', countryCode: 'MN', name: 'Omnogovi' }, { code: 'MN-035', countryCode: 'MN', name: 'Orhon' }, { code: 'MN-055', countryCode: 'MN', name: 'Ovorhangay' }, { code: 'MN-049', countryCode: 'MN', name: 'Selenge' }, { code: 'MN-051', countryCode: 'MN', name: 'Suhbaatar' }, { code: 'MN-047', countryCode: 'MN', name: 'Tov' }, { code: 'MN-1', countryCode: 'MN', name: 'Ulaanbaatar' }, { code: 'MN-046', countryCode: 'MN', name: 'Uvs' }, { code: 'MR-07', countryCode: 'MR', name: 'Adrar' }, { code: 'MR-03', countryCode: 'MR', name: 'Assaba' }, { code: 'MR-05', countryCode: 'MR', name: 'Brakna' }, { code: 'MR-08', countryCode: 'MR', name: 'Dakhlet Nouadhibou' }, { code: 'MR-04', countryCode: 'MR', name: 'Gorgol' }, { code: 'MR-10', countryCode: 'MR', name: 'Guidimaka' }, { code: 'MR-01', countryCode: 'MR', name: 'Hodh ech Chargui' }, { code: 'MR-02', countryCode: 'MR', name: 'Hodh el Gharbi' }, { code: 'MR-12', countryCode: 'MR', name: 'Inchiri' }, { code: 'MR-14', countryCode: 'MR', name: 'Nouakchott Nord' }, { code: 'MR-09', countryCode: 'MR', name: 'Tagant' }, { code: 'MR-11', countryCode: 'MR', name: 'Tiris Zemmour' }, { code: 'MR-06', countryCode: 'MR', name: 'Trarza' }, { code: 'MT-01', countryCode: 'MT', name: 'Attard' }, { code: 'MT-02', countryCode: 'MT', name: 'Balzan' }, { code: 'MT-04', countryCode: 'MT', name: 'Birkirkara' }, { code: 'MT-05', countryCode: 'MT', name: 'Birzebbuga' }, { code: 'MT-06', countryCode: 'MT', name: 'Bormla' }, { code: 'MT-07', countryCode: 'MT', name: 'Dingli' }, { code: 'MT-13', countryCode: 'MT', name: 'Ghajnsielem' }, { code: 'MT-15', countryCode: 'MT', name: 'Gharghur' }, { code: 'MT-17', countryCode: 'MT', name: 'Ghaxaq' }, { code: 'MT-64', countryCode: 'MT', name: 'Haz-Zabbar' }, { code: 'MT-60', countryCode: 'MT', name: 'Valletta' }, { code: 'MT-03', countryCode: 'MT', name: 'Birgu' }, { code: 'MT-08', countryCode: 'MT', name: 'Fgura' }, { code: 'MT-09', countryCode: 'MT', name: 'Floriana' }, { code: 'MT-11', countryCode: 'MT', name: 'Gudja' }, { code: 'MT-18', countryCode: 'MT', name: 'Hamrun' }, { code: 'MT-21', countryCode: 'MT', name: 'Kalkara' }, { code: 'MT-26', countryCode: 'MT', name: 'Marsa' }, { code: 'MT-30', countryCode: 'MT', name: 'Mellieha' }, { code: 'MT-32', countryCode: 'MT', name: 'Mosta' }, { code: 'MT-42', countryCode: 'MT', name: 'Qala' }, { code: 'MT-44', countryCode: 'MT', name: 'Qrendi' }, { code: 'MT-37', countryCode: 'MT', name: 'Nadur' }, { code: 'MT-38', countryCode: 'MT', name: 'Naxxar' }, { code: 'MT-46', countryCode: 'MT', name: 'Rabat Malta' }, { code: 'MT-55', countryCode: 'MT', name: 'Siggiewi' }, { code: 'MT-57', countryCode: 'MT', name: 'Swieqi' }, { code: 'MT-61', countryCode: 'MT', name: 'Xaghra' }, { code: 'MT-62', countryCode: 'MT', name: 'Xewkija' }, { code: 'MT-65', countryCode: 'MT', name: 'Zebbug Gozo' }, { code: 'MT-67', countryCode: 'MT', name: 'Zejtun' }, { code: 'MT-68', countryCode: 'MT', name: 'Zurrieq' }, { code: 'MT-23', countryCode: 'MT', name: 'Kirkop' }, { code: 'MT-19', countryCode: 'MT', name: 'Iklin' }, { code: 'MT-33', countryCode: 'MT', name: 'Mqabba' }, { code: 'MT-34', countryCode: 'MT', name: 'Msida' }, { code: 'MT-20', countryCode: 'MT', name: 'Isla' }, { code: 'MT-24', countryCode: 'MT', name: 'Lija' }, { code: 'MT-25', countryCode: 'MT', name: 'Luqa' }, { code: 'MT-28', countryCode: 'MT', name: 'Marsaxlokk' }, { code: 'MT-39', countryCode: 'MT', name: 'Paola' }, { code: 'MT-43', countryCode: 'MT', name: 'Qormi' }, { code: 'MT-47', countryCode: 'MT', name: 'Safi' }, { code: 'MT-49', countryCode: 'MT', name: 'Saint John' }, { code: 'MT-48', countryCode: 'MT', name: 'Saint Julian' }, { code: 'MT-53', countryCode: 'MT', name: 'Saint Lucia' }, { code: 'MT-51', countryCode: 'MT', name: "Saint Paul's Bay" }, { code: 'MT-54', countryCode: 'MT', name: 'Saint Venera' }, { code: 'MT-52', countryCode: 'MT', name: 'Sannat' }, { code: 'MT-22', countryCode: 'MT', name: 'Kercem' }, { code: 'MT-58', countryCode: 'MT', name: "Ta' Xbiex" }, { code: 'MT-59', countryCode: 'MT', name: 'Tarxien' }, { code: 'MT-56', countryCode: 'MT', name: 'Sliema' }, { code: 'MT-45', countryCode: 'MT', name: 'Rabat Gozo' }, { code: 'MU-BL', countryCode: 'MU', name: 'Black River' }, { code: 'MU-FL', countryCode: 'MU', name: 'Flacq' }, { code: 'MU-GP', countryCode: 'MU', name: 'Grand Port' }, { code: 'MU-MO', countryCode: 'MU', name: 'Moka' }, { code: 'MU-PA', countryCode: 'MU', name: 'Pamplemousses' }, { code: 'MU-PW', countryCode: 'MU', name: 'Plaines Wilhems' }, { code: 'MU-PU', countryCode: 'MU', name: 'Port Louis' }, { code: 'MU-RR', countryCode: 'MU', name: 'Riviere du Rempart' }, { code: 'MU-SA', countryCode: 'MU', name: 'Savanne' }, { code: 'MV-02', countryCode: 'MV', name: 'Alifu Alifu' }, { code: 'MV-20', countryCode: 'MV', name: 'Baa' }, { code: 'MV-17', countryCode: 'MV', name: 'Dhaalu' }, { code: 'MV-28', countryCode: 'MV', name: 'Gaafu Dhaalu' }, { code: 'MV-07', countryCode: 'MV', name: 'Haa Alifu' }, { code: 'MV-23', countryCode: 'MV', name: 'Haa Dhaalu' }, { code: 'MV-26', countryCode: 'MV', name: 'Kaafu' }, { code: 'MV-05', countryCode: 'MV', name: 'Laamu' }, { code: 'MV-MLE', countryCode: 'MV', name: 'Maale' }, { code: 'MV-12', countryCode: 'MV', name: 'Meemu' }, { code: 'MV-25', countryCode: 'MV', name: 'Noonu' }, { code: 'MV-13', countryCode: 'MV', name: 'Raa' }, { code: 'MV-01', countryCode: 'MV', name: 'Seenu' }, { code: 'MV-24', countryCode: 'MV', name: 'Shaviyani' }, { code: 'MV-08', countryCode: 'MV', name: 'Thaa' }, { code: 'MW-BA', countryCode: 'MW', name: 'Balaka' }, { code: 'MW-BL', countryCode: 'MW', name: 'Blantyre' }, { code: 'MW-CK', countryCode: 'MW', name: 'Chikwawa' }, { code: 'MW-CR', countryCode: 'MW', name: 'Chiradzulu' }, { code: 'MW-CT', countryCode: 'MW', name: 'Chitipa' }, { code: 'MW-DE', countryCode: 'MW', name: 'Dedza' }, { code: 'MW-DO', countryCode: 'MW', name: 'Dowa' }, { code: 'MW-KR', countryCode: 'MW', name: 'Karonga' }, { code: 'MW-KS', countryCode: 'MW', name: 'Kasungu' }, { code: 'MW-LK', countryCode: 'MW', name: 'Likoma' }, { code: 'MW-LI', countryCode: 'MW', name: 'Lilongwe' }, { code: 'MW-MH', countryCode: 'MW', name: 'Machinga' }, { code: 'MW-MG', countryCode: 'MW', name: 'Mangochi' }, { code: 'MW-MC', countryCode: 'MW', name: 'Mchinji' }, { code: 'MW-MU', countryCode: 'MW', name: 'Mulanje' }, { code: 'MW-MW', countryCode: 'MW', name: 'Mwanza' }, { code: 'MW-MZ', countryCode: 'MW', name: 'Mzimba' }, { code: 'MW-NE', countryCode: 'MW', name: 'Neno' }, { code: 'MW-NB', countryCode: 'MW', name: 'Nkhata Bay' }, { code: 'MW-NK', countryCode: 'MW', name: 'Nkhotakota' }, { code: 'MW-NS', countryCode: 'MW', name: 'Nsanje' }, { code: 'MW-NU', countryCode: 'MW', name: 'Ntcheu' }, { code: 'MW-NI', countryCode: 'MW', name: 'Ntchisi' }, { code: 'MW-PH', countryCode: 'MW', name: 'Phalombe' }, { code: 'MW-RU', countryCode: 'MW', name: 'Rumphi' }, { code: 'MW-SA', countryCode: 'MW', name: 'Salima' }, { code: 'MW-TH', countryCode: 'MW', name: 'Thyolo' }, { code: 'MW-ZO', countryCode: 'MW', name: 'Zomba' }, { code: 'MX-AGU', countryCode: 'MX', name: 'Aguascalientes' }, { code: 'MX-BCN', countryCode: 'MX', name: 'Baja California' }, { code: 'MX-BCS', countryCode: 'MX', name: 'Baja California Sur' }, { code: 'MX-CAM', countryCode: 'MX', name: 'Campeche' }, { code: 'MX-CHP', countryCode: 'MX', name: 'Chiapas' }, { code: 'MX-CHH', countryCode: 'MX', name: 'Chihuahua' }, { code: 'MX-CMX', countryCode: 'MX', name: 'Ciudad de Mexico' }, { code: 'MX-COA', countryCode: 'MX', name: 'Coahuila de Zaragoza' }, { code: 'MX-COL', countryCode: 'MX', name: 'Colima' }, { code: 'MX-DUR', countryCode: 'MX', name: 'Durango' }, { code: 'MX-GUA', countryCode: 'MX', name: 'Guanajuato' }, { code: 'MX-GRO', countryCode: 'MX', name: 'Guerrero' }, { code: 'MX-HID', countryCode: 'MX', name: 'Hidalgo' }, { code: 'MX-JAL', countryCode: 'MX', name: 'Jalisco' }, { code: 'MX-MEX', countryCode: 'MX', name: 'Mexico' }, { code: 'MX-MIC', countryCode: 'MX', name: 'Michoacan de Ocampo' }, { code: 'MX-MOR', countryCode: 'MX', name: 'Morelos' }, { code: 'MX-NAY', countryCode: 'MX', name: 'Nayarit' }, { code: 'MX-NLE', countryCode: 'MX', name: 'Nuevo Leon' }, { code: 'MX-OAX', countryCode: 'MX', name: 'Oaxaca' }, { code: 'MX-PUE', countryCode: 'MX', name: 'Puebla' }, { code: 'MX-QUE', countryCode: 'MX', name: 'Queretaro' }, { code: 'MX-ROO', countryCode: 'MX', name: 'Quintana Roo' }, { code: 'MX-SLP', countryCode: 'MX', name: 'San Luis Potosi' }, { code: 'MX-SIN', countryCode: 'MX', name: 'Sinaloa' }, { code: 'MX-SON', countryCode: 'MX', name: 'Sonora' }, { code: 'MX-TAB', countryCode: 'MX', name: 'Tabasco' }, { code: 'MX-TAM', countryCode: 'MX', name: 'Tamaulipas' }, { code: 'MX-TLA', countryCode: 'MX', name: 'Tlaxcala' }, { code: 'MX-VER', countryCode: 'MX', name: 'Veracruz de Ignacio de la Llave' }, { code: 'MX-YUC', countryCode: 'MX', name: 'Yucatan' }, { code: 'MX-ZAC', countryCode: 'MX', name: 'Zacatecas' }, { code: 'MY-01', countryCode: 'MY', name: 'Johor' }, { code: 'MY-02', countryCode: 'MY', name: 'Kedah' }, { code: 'MY-03', countryCode: 'MY', name: 'Kelantan' }, { code: 'MY-04', countryCode: 'MY', name: 'Melaka' }, { code: 'MY-05', countryCode: 'MY', name: 'Negeri Sembilan' }, { code: 'MY-06', countryCode: 'MY', name: 'Pahang' }, { code: 'MY-08', countryCode: 'MY', name: 'Perak' }, { code: 'MY-09', countryCode: 'MY', name: 'Perlis' }, { code: 'MY-07', countryCode: 'MY', name: 'Pulau Pinang' }, { code: 'MY-12', countryCode: 'MY', name: 'Sabah' }, { code: 'MY-13', countryCode: 'MY', name: 'Sarawak' }, { code: 'MY-10', countryCode: 'MY', name: 'Selangor' }, { code: 'MY-11', countryCode: 'MY', name: 'Terengganu' }, { code: 'MY-14', countryCode: 'MY', name: 'Wilayah Persekutuan Kuala Lumpur' }, { code: 'MY-15', countryCode: 'MY', name: 'Wilayah Persekutuan Labuan' }, { code: 'MY-16', countryCode: 'MY', name: 'Wilayah Persekutuan Putrajaya' }, { code: 'MZ-P', countryCode: 'MZ', name: 'Cabo Delgado' }, { code: 'MZ-G', countryCode: 'MZ', name: 'Gaza' }, { code: 'MZ-I', countryCode: 'MZ', name: 'Inhambane' }, { code: 'MZ-B', countryCode: 'MZ', name: 'Manica' }, { code: 'MZ-MPM', countryCode: 'MZ', name: 'Maputo' }, { code: 'MZ-N', countryCode: 'MZ', name: 'Nampula' }, { code: 'MZ-A', countryCode: 'MZ', name: 'Niassa' }, { code: 'MZ-S', countryCode: 'MZ', name: 'Sofala' }, { code: 'MZ-T', countryCode: 'MZ', name: 'Tete' }, { code: 'MZ-Q', countryCode: 'MZ', name: 'Zambezia' }, { code: 'NA-ER', countryCode: 'NA', name: 'Erongo' }, { code: 'NA-HA', countryCode: 'NA', name: 'Hardap' }, { code: 'NA-KA', countryCode: 'NA', name: 'Karas' }, { code: 'NA-KE', countryCode: 'NA', name: 'Kavango East' }, { code: 'NA-KH', countryCode: 'NA', name: 'Khomas' }, { code: 'NA-KU', countryCode: 'NA', name: 'Kunene' }, { code: 'NA-OW', countryCode: 'NA', name: 'Ohangwena' }, { code: 'NA-OH', countryCode: 'NA', name: 'Omaheke' }, { code: 'NA-OS', countryCode: 'NA', name: 'Omusati' }, { code: 'NA-ON', countryCode: 'NA', name: 'Oshana' }, { code: 'NA-OT', countryCode: 'NA', name: 'Oshikoto' }, { code: 'NA-OD', countryCode: 'NA', name: 'Otjozondjupa' }, { code: 'NA-CA', countryCode: 'NA', name: 'Zambezi' }, { code: 'NE-1', countryCode: 'NE', name: 'Agadez' }, { code: 'NE-2', countryCode: 'NE', name: 'Diffa' }, { code: 'NE-3', countryCode: 'NE', name: 'Dosso' }, { code: 'NE-4', countryCode: 'NE', name: 'Maradi' }, { code: 'NE-8', countryCode: 'NE', name: 'Niamey' }, { code: 'NE-5', countryCode: 'NE', name: 'Tahoua' }, { code: 'NE-6', countryCode: 'NE', name: 'Tillaberi' }, { code: 'NE-7', countryCode: 'NE', name: 'Zinder' }, { code: 'NG-AB', countryCode: 'NG', name: 'Abia' }, { code: 'NG-FC', countryCode: 'NG', name: 'Abuja Federal Capital Territory' }, { code: 'NG-AD', countryCode: 'NG', name: 'Adamawa' }, { code: 'NG-AK', countryCode: 'NG', name: 'Akwa Ibom' }, { code: 'NG-AN', countryCode: 'NG', name: 'Anambra' }, { code: 'NG-BA', countryCode: 'NG', name: 'Bauchi' }, { code: 'NG-BY', countryCode: 'NG', name: 'Bayelsa' }, { code: 'NG-BE', countryCode: 'NG', name: 'Benue' }, { code: 'NG-BO', countryCode: 'NG', name: 'Borno' }, { code: 'NG-CR', countryCode: 'NG', name: 'Cross River' }, { code: 'NG-DE', countryCode: 'NG', name: 'Delta' }, { code: 'NG-EB', countryCode: 'NG', name: 'Ebonyi' }, { code: 'NG-ED', countryCode: 'NG', name: 'Edo' }, { code: 'NG-EK', countryCode: 'NG', name: 'Ekiti' }, { code: 'NG-EN', countryCode: 'NG', name: 'Enugu' }, { code: 'NG-GO', countryCode: 'NG', name: 'Gombe' }, { code: 'NG-IM', countryCode: 'NG', name: 'Imo' }, { code: 'NG-JI', countryCode: 'NG', name: 'Jigawa' }, { code: 'NG-KD', countryCode: 'NG', name: 'Kaduna' }, { code: 'NG-KN', countryCode: 'NG', name: 'Kano' }, { code: 'NG-KT', countryCode: 'NG', name: 'Katsina' }, { code: 'NG-KE', countryCode: 'NG', name: 'Kebbi' }, { code: 'NG-KO', countryCode: 'NG', name: 'Kogi' }, { code: 'NG-KW', countryCode: 'NG', name: 'Kwara' }, { code: 'NG-LA', countryCode: 'NG', name: 'Lagos' }, { code: 'NG-NA', countryCode: 'NG', name: 'Nasarawa' }, { code: 'NG-NI', countryCode: 'NG', name: 'Niger' }, { code: 'NG-OG', countryCode: 'NG', name: 'Ogun' }, { code: 'NG-ON', countryCode: 'NG', name: 'Ondo' }, { code: 'NG-OS', countryCode: 'NG', name: 'Osun' }, { code: 'NG-OY', countryCode: 'NG', name: 'Oyo' }, { code: 'NG-PL', countryCode: 'NG', name: 'Plateau' }, { code: 'NG-RI', countryCode: 'NG', name: 'Rivers' }, { code: 'NG-SO', countryCode: 'NG', name: 'Sokoto' }, { code: 'NG-TA', countryCode: 'NG', name: 'Taraba' }, { code: 'NG-YO', countryCode: 'NG', name: 'Yobe' }, { code: 'NG-ZA', countryCode: 'NG', name: 'Zamfara' }, { code: 'NI-AN', countryCode: 'NI', name: 'Atlantico Norte' }, { code: 'NI-AS', countryCode: 'NI', name: 'Atlantico Sur' }, { code: 'NI-BO', countryCode: 'NI', name: 'Boaco' }, { code: 'NI-CA', countryCode: 'NI', name: 'Carazo' }, { code: 'NI-CI', countryCode: 'NI', name: 'Chinandega' }, { code: 'NI-CO', countryCode: 'NI', name: 'Chontales' }, { code: 'NI-ES', countryCode: 'NI', name: 'Esteli' }, { code: 'NI-GR', countryCode: 'NI', name: 'Granada' }, { code: 'NI-JI', countryCode: 'NI', name: 'Jinotega' }, { code: 'NI-LE', countryCode: 'NI', name: 'Leon' }, { code: 'NI-MD', countryCode: 'NI', name: 'Madriz' }, { code: 'NI-MN', countryCode: 'NI', name: 'Managua' }, { code: 'NI-MS', countryCode: 'NI', name: 'Masaya' }, { code: 'NI-MT', countryCode: 'NI', name: 'Matagalpa' }, { code: 'NI-NS', countryCode: 'NI', name: 'Nueva Segovia' }, { code: 'NI-SJ', countryCode: 'NI', name: 'Rio San Juan' }, { code: 'NI-RI', countryCode: 'NI', name: 'Rivas' }, { code: 'NL-DR', countryCode: 'NL', name: 'Drenthe' }, { code: 'NL-FL', countryCode: 'NL', name: 'Flevoland' }, { code: 'NL-FR', countryCode: 'NL', name: 'Fryslan' }, { code: 'NL-GE', countryCode: 'NL', name: 'Gelderland' }, { code: 'NL-GR', countryCode: 'NL', name: 'Groningen' }, { code: 'NL-LI', countryCode: 'NL', name: 'Limburg' }, { code: 'NL-NB', countryCode: 'NL', name: 'Noord-Brabant' }, { code: 'NL-NH', countryCode: 'NL', name: 'Noord-Holland' }, { code: 'NL-OV', countryCode: 'NL', name: 'Overijssel' }, { code: 'NL-UT', countryCode: 'NL', name: 'Utrecht' }, { code: 'NL-ZE', countryCode: 'NL', name: 'Zeeland' }, { code: 'NL-ZH', countryCode: 'NL', name: 'Zuid-Holland' }, { code: 'NO-02', countryCode: 'NO', name: 'Akershus' }, { code: 'NO-09', countryCode: 'NO', name: 'Aust-Agder' }, { code: 'NO-06', countryCode: 'NO', name: 'Buskerud' }, { code: 'NO-20', countryCode: 'NO', name: 'Finnmark' }, { code: 'NO-04', countryCode: 'NO', name: 'Hedmark' }, { code: 'NO-12', countryCode: 'NO', name: 'Hordaland' }, { code: 'NO-15', countryCode: 'NO', name: 'More og Romsdal' }, { code: 'NO-17', countryCode: 'NO', name: 'Nord-Trondelag' }, { code: 'NO-18', countryCode: 'NO', name: 'Nordland' }, { code: 'NO-05', countryCode: 'NO', name: 'Oppland' }, { code: 'NO-03', countryCode: 'NO', name: 'Oslo' }, { code: 'NO-01', countryCode: 'NO', name: 'Ostfold' }, { code: 'NO-11', countryCode: 'NO', name: 'Rogaland' }, { code: 'NO-14', countryCode: 'NO', name: 'Sogn og Fjordane' }, { code: 'NO-16', countryCode: 'NO', name: 'Sor-Trondelag' }, { code: 'NO-08', countryCode: 'NO', name: 'Telemark' }, { code: 'NO-19', countryCode: 'NO', name: 'Troms' }, { code: 'NO-10', countryCode: 'NO', name: 'Vest-Agder' }, { code: 'NO-07', countryCode: 'NO', name: 'Vestfold' }, { code: 'NP-BA', countryCode: 'NP', name: 'Bagmati' }, { code: 'NP-BH', countryCode: 'NP', name: 'Bheri' }, { code: 'NP-DH', countryCode: 'NP', name: 'Dhawalagiri' }, { code: 'NP-GA', countryCode: 'NP', name: 'Gandaki' }, { code: 'NP-JA', countryCode: 'NP', name: 'Janakpur' }, { code: 'NP-KA', countryCode: 'NP', name: 'Karnali' }, { code: 'NP-KO', countryCode: 'NP', name: 'Kosi' }, { code: 'NP-LU', countryCode: 'NP', name: 'Lumbini' }, { code: 'NP-MA', countryCode: 'NP', name: 'Mahakali' }, { code: 'NP-ME', countryCode: 'NP', name: 'Mechi' }, { code: 'NP-NA', countryCode: 'NP', name: 'Narayani' }, { code: 'NP-RA', countryCode: 'NP', name: 'Rapti' }, { code: 'NP-SA', countryCode: 'NP', name: 'Sagarmatha' }, { code: 'NP-SE', countryCode: 'NP', name: 'Seti' }, { code: 'NR-14', countryCode: 'NR', name: 'Yaren' }, { code: 'NZ-AUK', countryCode: 'NZ', name: 'Auckland' }, { code: 'NZ-BOP', countryCode: 'NZ', name: 'Bay of Plenty' }, { code: 'NZ-CAN', countryCode: 'NZ', name: 'Canterbury' }, { code: 'NZ-CIT', countryCode: 'NZ', name: 'Chatham Islands Territory' }, { code: 'NZ-GIS', countryCode: 'NZ', name: 'Gisborne' }, { code: 'NZ-HKB', countryCode: 'NZ', name: "Hawke's Bay" }, { code: 'NZ-MWT', countryCode: 'NZ', name: 'Manawatu-Wanganui' }, { code: 'NZ-MBH', countryCode: 'NZ', name: 'Marlborough' }, { code: 'NZ-NSN', countryCode: 'NZ', name: 'Nelson' }, { code: 'NZ-NTL', countryCode: 'NZ', name: 'Northland' }, { code: 'NZ-OTA', countryCode: 'NZ', name: 'Otago' }, { code: 'NZ-STL', countryCode: 'NZ', name: 'Southland' }, { code: 'NZ-TKI', countryCode: 'NZ', name: 'Taranaki' }, { code: 'NZ-TAS', countryCode: 'NZ', name: 'Tasman' }, { code: 'NZ-WKO', countryCode: 'NZ', name: 'Waikato' }, { code: 'NZ-WGN', countryCode: 'NZ', name: 'Wellington' }, { code: 'NZ-WTC', countryCode: 'NZ', name: 'West Coast' }, { code: 'OM-DA', countryCode: 'OM', name: 'Ad Dakhiliyah' }, { code: 'OM-BU', countryCode: 'OM', name: 'Al Buraymi' }, { code: 'OM-WU', countryCode: 'OM', name: 'Al Wusta' }, { code: 'OM-ZA', countryCode: 'OM', name: 'Az Zahirah' }, { code: 'OM-BJ', countryCode: 'OM', name: 'Janub al Batinah' }, { code: 'OM-SJ', countryCode: 'OM', name: 'Janub ash Sharqiyah' }, { code: 'OM-MA', countryCode: 'OM', name: 'Masqat' }, { code: 'OM-MU', countryCode: 'OM', name: 'Musandam' }, { code: 'OM-BS', countryCode: 'OM', name: 'Shamal al Batinah' }, { code: 'OM-SS', countryCode: 'OM', name: 'Shamal ash Sharqiyah' }, { code: 'OM-ZU', countryCode: 'OM', name: 'Zufar' }, { code: 'PA-1', countryCode: 'PA', name: 'Bocas del Toro' }, { code: 'PA-4', countryCode: 'PA', name: 'Chiriqui' }, { code: 'PA-2', countryCode: 'PA', name: 'Cocle' }, { code: 'PA-3', countryCode: 'PA', name: 'Colon' }, { code: 'PA-5', countryCode: 'PA', name: 'Darien' }, { code: 'PA-6', countryCode: 'PA', name: 'Herrera' }, { code: 'PA-7', countryCode: 'PA', name: 'Los Santos' }, { code: 'PA-8', countryCode: 'PA', name: 'Panama' }, { code: 'PA-9', countryCode: 'PA', name: 'Veraguas' }, { code: 'PE-AMA', countryCode: 'PE', name: 'Amazonas' }, { code: 'PE-ANC', countryCode: 'PE', name: 'Ancash' }, { code: 'PE-APU', countryCode: 'PE', name: 'Apurimac' }, { code: 'PE-ARE', countryCode: 'PE', name: 'Arequipa' }, { code: 'PE-AYA', countryCode: 'PE', name: 'Ayacucho' }, { code: 'PE-CAJ', countryCode: 'PE', name: 'Cajamarca' }, { code: 'PE-CUS', countryCode: 'PE', name: 'Cusco' }, { code: 'PE-CAL', countryCode: 'PE', name: 'El Callao' }, { code: 'PE-HUV', countryCode: 'PE', name: 'Huancavelica' }, { code: 'PE-HUC', countryCode: 'PE', name: 'Huanuco' }, { code: 'PE-ICA', countryCode: 'PE', name: 'Ica' }, { code: 'PE-JUN', countryCode: 'PE', name: 'Junin' }, { code: 'PE-LAL', countryCode: 'PE', name: 'La Libertad' }, { code: 'PE-LAM', countryCode: 'PE', name: 'Lambayeque' }, { code: 'PE-LIM', countryCode: 'PE', name: 'Lima' }, { code: 'PE-LOR', countryCode: 'PE', name: 'Loreto' }, { code: 'PE-MDD', countryCode: 'PE', name: 'Madre de Dios' }, { code: 'PE-MOQ', countryCode: 'PE', name: 'Moquegua' }, { code: 'PE-PAS', countryCode: 'PE', name: 'Pasco' }, { code: 'PE-PIU', countryCode: 'PE', name: 'Piura' }, { code: 'PE-PUN', countryCode: 'PE', name: 'Puno' }, { code: 'PE-SAM', countryCode: 'PE', name: 'San Martin' }, { code: 'PE-TAC', countryCode: 'PE', name: 'Tacna' }, { code: 'PE-TUM', countryCode: 'PE', name: 'Tumbes' }, { code: 'PE-UCA', countryCode: 'PE', name: 'Ucayali' }, { code: 'PG-NSB', countryCode: 'PG', name: 'Bougainville' }, { code: 'PG-CPK', countryCode: 'PG', name: 'Chimbu' }, { code: 'PG-EBR', countryCode: 'PG', name: 'East New Britain' }, { code: 'PG-ESW', countryCode: 'PG', name: 'East Sepik' }, { code: 'PG-EHG', countryCode: 'PG', name: 'Eastern Highlands' }, { code: 'PG-EPW', countryCode: 'PG', name: 'Enga' }, { code: 'PG-GPK', countryCode: 'PG', name: 'Gulf' }, { code: 'PG-MPM', countryCode: 'PG', name: 'Madang' }, { code: 'PG-MRL', countryCode: 'PG', name: 'Manus' }, { code: 'PG-MBA', countryCode: 'PG', name: 'Milne Bay' }, { code: 'PG-MPL', countryCode: 'PG', name: 'Morobe' }, { code: 'PG-NCD', countryCode: 'PG', name: 'National Capital District (Port Moresby)' }, { code: 'PG-NIK', countryCode: 'PG', name: 'New Ireland' }, { code: 'PG-NPP', countryCode: 'PG', name: 'Northern' }, { code: 'PG-SHM', countryCode: 'PG', name: 'Southern Highlands' }, { code: 'PG-WBK', countryCode: 'PG', name: 'West New Britain' }, { code: 'PG-SAN', countryCode: 'PG', name: 'West Sepik' }, { code: 'PG-WPD', countryCode: 'PG', name: 'Western' }, { code: 'PG-WHM', countryCode: 'PG', name: 'Western Highlands' }, { code: 'PH-ABR', countryCode: 'PH', name: 'Abra' }, { code: 'PH-AGN', countryCode: 'PH', name: 'Agusan del Norte' }, { code: 'PH-AGS', countryCode: 'PH', name: 'Agusan del Sur' }, { code: 'PH-AKL', countryCode: 'PH', name: 'Aklan' }, { code: 'PH-ALB', countryCode: 'PH', name: 'Albay' }, { code: 'PH-ANT', countryCode: 'PH', name: 'Antique' }, { code: 'PH-APA', countryCode: 'PH', name: 'Apayao' }, { code: 'PH-AUR', countryCode: 'PH', name: 'Aurora' }, { code: 'PH-BAS', countryCode: 'PH', name: 'Basilan' }, { code: 'PH-BAN', countryCode: 'PH', name: 'Bataan' }, { code: 'PH-BTN', countryCode: 'PH', name: 'Batanes' }, { code: 'PH-BTG', countryCode: 'PH', name: 'Batangas' }, { code: 'PH-BEN', countryCode: 'PH', name: 'Benguet' }, { code: 'PH-BOH', countryCode: 'PH', name: 'Bohol' }, { code: 'PH-BUK', countryCode: 'PH', name: 'Bukidnon' }, { code: 'PH-BUL', countryCode: 'PH', name: 'Bulacan' }, { code: 'PH-CAG', countryCode: 'PH', name: 'Cagayan' }, { code: 'PH-CAN', countryCode: 'PH', name: 'Camarines Norte' }, { code: 'PH-CAS', countryCode: 'PH', name: 'Camarines Sur' }, { code: 'PH-CAM', countryCode: 'PH', name: 'Camiguin' }, { code: 'PH-CAP', countryCode: 'PH', name: 'Capiz' }, { code: 'PH-CAT', countryCode: 'PH', name: 'Catanduanes' }, { code: 'PH-CAV', countryCode: 'PH', name: 'Cavite' }, { code: 'PH-CEB', countryCode: 'PH', name: 'Cebu' }, { code: 'PH-NCO', countryCode: 'PH', name: 'Cotabato' }, { code: 'PH-DAS', countryCode: 'PH', name: 'Davao del Sur' }, { code: 'PH-DAO', countryCode: 'PH', name: 'Davao Oriental' }, { code: 'PH-EAS', countryCode: 'PH', name: 'Eastern Samar' }, { code: 'PH-IFU', countryCode: 'PH', name: 'Ifugao' }, { code: 'PH-ILN', countryCode: 'PH', name: 'Ilocos Norte' }, { code: 'PH-ILS', countryCode: 'PH', name: 'Ilocos Sur' }, { code: 'PH-ILI', countryCode: 'PH', name: 'Iloilo' }, { code: 'PH-ISA', countryCode: 'PH', name: 'Isabela' }, { code: 'PH-KAL', countryCode: 'PH', name: 'Kalinga' }, { code: 'PH-LUN', countryCode: 'PH', name: 'La Union' }, { code: 'PH-LAG', countryCode: 'PH', name: 'Laguna' }, { code: 'PH-LAN', countryCode: 'PH', name: 'Lanao del Norte' }, { code: 'PH-LAS', countryCode: 'PH', name: 'Lanao del Sur' }, { code: 'PH-LEY', countryCode: 'PH', name: 'Leyte' }, { code: 'PH-MAG', countryCode: 'PH', name: 'Maguindanao' }, { code: 'PH-MAD', countryCode: 'PH', name: 'Marinduque' }, { code: 'PH-MAS', countryCode: 'PH', name: 'Masbate' }, { code: 'PH-MDC', countryCode: 'PH', name: 'Mindoro Occidental' }, { code: 'PH-MDR', countryCode: 'PH', name: 'Mindoro Oriental' }, { code: 'PH-MSC', countryCode: 'PH', name: 'Misamis Occidental' }, { code: 'PH-MSR', countryCode: 'PH', name: 'Misamis Oriental' }, { code: 'PH-MOU', countryCode: 'PH', name: 'Mountain Province' }, { code: 'PH-00', countryCode: 'PH', name: 'National Capital Region' }, { code: 'PH-NEC', countryCode: 'PH', name: 'Negros Occidental' }, { code: 'PH-NER', countryCode: 'PH', name: 'Negros Oriental' }, { code: 'PH-NSA', countryCode: 'PH', name: 'Northern Samar' }, { code: 'PH-NUE', countryCode: 'PH', name: 'Nueva Ecija' }, { code: 'PH-NUV', countryCode: 'PH', name: 'Nueva Vizcaya' }, { code: 'PH-PLW', countryCode: 'PH', name: 'Palawan' }, { code: 'PH-PAM', countryCode: 'PH', name: 'Pampanga' }, { code: 'PH-PAN', countryCode: 'PH', name: 'Pangasinan' }, { code: 'PH-QUE', countryCode: 'PH', name: 'Quezon' }, { code: 'PH-QUI', countryCode: 'PH', name: 'Quirino' }, { code: 'PH-RIZ', countryCode: 'PH', name: 'Rizal' }, { code: 'PH-ROM', countryCode: 'PH', name: 'Romblon' }, { code: 'PH-WSA', countryCode: 'PH', name: 'Samar' }, { code: 'PH-SIG', countryCode: 'PH', name: 'Siquijor' }, { code: 'PH-SOR', countryCode: 'PH', name: 'Sorsogon' }, { code: 'PH-SCO', countryCode: 'PH', name: 'South Cotabato' }, { code: 'PH-SLE', countryCode: 'PH', name: 'Southern Leyte' }, { code: 'PH-SUK', countryCode: 'PH', name: 'Sultan Kudarat' }, { code: 'PH-SLU', countryCode: 'PH', name: 'Sulu' }, { code: 'PH-SUN', countryCode: 'PH', name: 'Surigao del Norte' }, { code: 'PH-SUR', countryCode: 'PH', name: 'Surigao del Sur' }, { code: 'PH-TAR', countryCode: 'PH', name: 'Tarlac' }, { code: 'PH-TAW', countryCode: 'PH', name: 'Tawi-Tawi' }, { code: 'PH-ZMB', countryCode: 'PH', name: 'Zambales' }, { code: 'PH-ZAN', countryCode: 'PH', name: 'Zamboanga del Norte' }, { code: 'PH-ZAS', countryCode: 'PH', name: 'Zamboanga del Sur' }, { code: 'PK-JK', countryCode: 'PK', name: 'Azad Kashmir' }, { code: 'PK-BA', countryCode: 'PK', name: 'Balochistan' }, { code: 'PK-TA', countryCode: 'PK', name: 'Federally Administered Tribal Areas' }, { code: 'PK-GB', countryCode: 'PK', name: 'Gilgit-Baltistan' }, { code: 'PK-IS', countryCode: 'PK', name: 'Islamabad' }, { code: 'PK-KP', countryCode: 'PK', name: 'Khyber Pakhtunkhwa' }, { code: 'PK-PB', countryCode: 'PK', name: 'Punjab' }, { code: 'PK-SD', countryCode: 'PK', name: 'Sindh' }, { code: 'PL-DS', countryCode: 'PL', name: 'Dolnoslaskie' }, { code: 'PL-KP', countryCode: 'PL', name: 'Kujawsko-pomorskie' }, { code: 'PL-LD', countryCode: 'PL', name: 'Lodzkie' }, { code: 'PL-LU', countryCode: 'PL', name: 'Lubelskie' }, { code: 'PL-LB', countryCode: 'PL', name: 'Lubuskie' }, { code: 'PL-MA', countryCode: 'PL', name: 'Malopolskie' }, { code: 'PL-MZ', countryCode: 'PL', name: 'Mazowieckie' }, { code: 'PL-OP', countryCode: 'PL', name: 'Opolskie' }, { code: 'PL-PK', countryCode: 'PL', name: 'Podkarpackie' }, { code: 'PL-PD', countryCode: 'PL', name: 'Podlaskie' }, { code: 'PL-PM', countryCode: 'PL', name: 'Pomorskie' }, { code: 'PL-SL', countryCode: 'PL', name: 'Slaskie' }, { code: 'PL-SK', countryCode: 'PL', name: 'Swietokrzyskie' }, { code: 'PL-WN', countryCode: 'PL', name: 'Warminsko-mazurskie' }, { code: 'PL-WP', countryCode: 'PL', name: 'Wielkopolskie' }, { code: 'PL-ZP', countryCode: 'PL', name: 'Zachodniopomorskie' }, { code: 'PS-BTH', countryCode: 'PS', name: 'Bethlehem' }, { code: 'PS-GZA', countryCode: 'PS', name: 'Gaza' }, { code: 'PS-HBN', countryCode: 'PS', name: 'Hebron' }, { code: 'PS-JEN', countryCode: 'PS', name: 'Jenin' }, { code: 'PS-JRH', countryCode: 'PS', name: 'Jericho and Al Aghwar' }, { code: 'PS-JEM', countryCode: 'PS', name: 'Jerusalem' }, { code: 'PS-NBS', countryCode: 'PS', name: 'Nablus' }, { code: 'PS-QQA', countryCode: 'PS', name: 'Qalqilya' }, { code: 'PS-RBH', countryCode: 'PS', name: 'Ramallah' }, { code: 'PS-SLT', countryCode: 'PS', name: 'Salfit' }, { code: 'PS-TBS', countryCode: 'PS', name: 'Tubas' }, { code: 'PS-TKM', countryCode: 'PS', name: 'Tulkarm' }, { code: 'PT-01', countryCode: 'PT', name: 'Aveiro' }, { code: 'PT-02', countryCode: 'PT', name: 'Beja' }, { code: 'PT-03', countryCode: 'PT', name: 'Braga' }, { code: 'PT-04', countryCode: 'PT', name: 'Braganca' }, { code: 'PT-05', countryCode: 'PT', name: 'Castelo Branco' }, { code: 'PT-06', countryCode: 'PT', name: 'Coimbra' }, { code: 'PT-07', countryCode: 'PT', name: 'Evora' }, { code: 'PT-08', countryCode: 'PT', name: 'Faro' }, { code: 'PT-09', countryCode: 'PT', name: 'Guarda' }, { code: 'PT-10', countryCode: 'PT', name: 'Leiria' }, { code: 'PT-11', countryCode: 'PT', name: 'Lisboa' }, { code: 'PT-12', countryCode: 'PT', name: 'Portalegre' }, { code: 'PT-13', countryCode: 'PT', name: 'Porto' }, { code: 'PT-30', countryCode: 'PT', name: 'Regiao Autonoma da Madeira' }, { code: 'PT-20', countryCode: 'PT', name: 'Regiao Autonoma dos Acores' }, { code: 'PT-14', countryCode: 'PT', name: 'Santarem' }, { code: 'PT-15', countryCode: 'PT', name: 'Setubal' }, { code: 'PT-16', countryCode: 'PT', name: 'Viana do Castelo' }, { code: 'PT-17', countryCode: 'PT', name: 'Vila Real' }, { code: 'PT-18', countryCode: 'PT', name: 'Viseu' }, { code: 'PW-002', countryCode: 'PW', name: 'Aimeliik' }, { code: 'PW-004', countryCode: 'PW', name: 'Airai' }, { code: 'PW-010', countryCode: 'PW', name: 'Angaur' }, { code: 'PW-100', countryCode: 'PW', name: 'Kayangel' }, { code: 'PW-150', countryCode: 'PW', name: 'Koror' }, { code: 'PW-212', countryCode: 'PW', name: 'Melekeok' }, { code: 'PW-214', countryCode: 'PW', name: 'Ngaraard' }, { code: 'PW-218', countryCode: 'PW', name: 'Ngarchelong' }, { code: 'PW-222', countryCode: 'PW', name: 'Ngardmau' }, { code: 'PW-224', countryCode: 'PW', name: 'Ngatpang' }, { code: 'PW-228', countryCode: 'PW', name: 'Ngiwal' }, { code: 'PW-350', countryCode: 'PW', name: 'Peleliu' }, { code: 'PY-16', countryCode: 'PY', name: 'Alto Paraguay' }, { code: 'PY-10', countryCode: 'PY', name: 'Alto Parana' }, { code: 'PY-13', countryCode: 'PY', name: 'Amambay' }, { code: 'PY-ASU', countryCode: 'PY', name: 'Asuncion' }, { code: 'PY-19', countryCode: 'PY', name: 'Boqueron' }, { code: 'PY-5', countryCode: 'PY', name: 'Caaguazu' }, { code: 'PY-6', countryCode: 'PY', name: 'Caazapa' }, { code: 'PY-14', countryCode: 'PY', name: 'Canindeyu' }, { code: 'PY-11', countryCode: 'PY', name: 'Central' }, { code: 'PY-1', countryCode: 'PY', name: 'Concepcion' }, { code: 'PY-3', countryCode: 'PY', name: 'Cordillera' }, { code: 'PY-4', countryCode: 'PY', name: 'Guaira' }, { code: 'PY-7', countryCode: 'PY', name: 'Itapua' }, { code: 'PY-8', countryCode: 'PY', name: 'Misiones' }, { code: 'PY-12', countryCode: 'PY', name: 'Neembucu' }, { code: 'PY-9', countryCode: 'PY', name: 'Paraguari' }, { code: 'PY-15', countryCode: 'PY', name: 'Presidente Hayes' }, { code: 'PY-2', countryCode: 'PY', name: 'San Pedro' }, { code: 'QA-DA', countryCode: 'QA', name: 'Ad Dawhah' }, { code: 'QA-KH', countryCode: 'QA', name: 'Al Khawr wa adh Dhakhirah' }, { code: 'QA-WA', countryCode: 'QA', name: 'Al Wakrah' }, { code: 'QA-RA', countryCode: 'QA', name: 'Ar Rayyan' }, { code: 'QA-MS', countryCode: 'QA', name: 'Ash Shamal' }, { code: 'QA-ZA', countryCode: 'QA', name: "Az Za'ayin" }, { code: 'QA-US', countryCode: 'QA', name: 'Umm Salal' }, { code: 'RO-AB', countryCode: 'RO', name: 'Alba' }, { code: 'RO-AR', countryCode: 'RO', name: 'Arad' }, { code: 'RO-AG', countryCode: 'RO', name: 'Arges' }, { code: 'RO-BC', countryCode: 'RO', name: 'Bacau' }, { code: 'RO-BH', countryCode: 'RO', name: 'Bihor' }, { code: 'RO-BN', countryCode: 'RO', name: 'Bistrita-Nasaud' }, { code: 'RO-BT', countryCode: 'RO', name: 'Botosani' }, { code: 'RO-BR', countryCode: 'RO', name: 'Braila' }, { code: 'RO-BV', countryCode: 'RO', name: 'Brasov' }, { code: 'RO-B', countryCode: 'RO', name: 'Bucuresti' }, { code: 'RO-BZ', countryCode: 'RO', name: 'Buzau' }, { code: 'RO-CL', countryCode: 'RO', name: 'Calarasi' }, { code: 'RO-CS', countryCode: 'RO', name: 'Caras-Severin' }, { code: 'RO-CJ', countryCode: 'RO', name: 'Cluj' }, { code: 'RO-CT', countryCode: 'RO', name: 'Constanta' }, { code: 'RO-CV', countryCode: 'RO', name: 'Covasna' }, { code: 'RO-DB', countryCode: 'RO', name: 'Dambovita' }, { code: 'RO-DJ', countryCode: 'RO', name: 'Dolj' }, { code: 'RO-GL', countryCode: 'RO', name: 'Galati' }, { code: 'RO-GR', countryCode: 'RO', name: 'Giurgiu' }, { code: 'RO-GJ', countryCode: 'RO', name: 'Gorj' }, { code: 'RO-HR', countryCode: 'RO', name: 'Harghita' }, { code: 'RO-HD', countryCode: 'RO', name: 'Hunedoara' }, { code: 'RO-IL', countryCode: 'RO', name: 'Ialomita' }, { code: 'RO-IS', countryCode: 'RO', name: 'Iasi' }, { code: 'RO-IF', countryCode: 'RO', name: 'Ilfov' }, { code: 'RO-MM', countryCode: 'RO', name: 'Maramures' }, { code: 'RO-MH', countryCode: 'RO', name: 'Mehedinti' }, { code: 'RO-MS', countryCode: 'RO', name: 'Mures' }, { code: 'RO-NT', countryCode: 'RO', name: 'Neamt' }, { code: 'RO-OT', countryCode: 'RO', name: 'Olt' }, { code: 'RO-PH', countryCode: 'RO', name: 'Prahova' }, { code: 'RO-SJ', countryCode: 'RO', name: 'Salaj' }, { code: 'RO-SM', countryCode: 'RO', name: 'Satu Mare' }, { code: 'RO-SB', countryCode: 'RO', name: 'Sibiu' }, { code: 'RO-SV', countryCode: 'RO', name: 'Suceava' }, { code: 'RO-TR', countryCode: 'RO', name: 'Teleorman' }, { code: 'RO-TM', countryCode: 'RO', name: 'Timis' }, { code: 'RO-TL', countryCode: 'RO', name: 'Tulcea' }, { code: 'RO-VL', countryCode: 'RO', name: 'Valcea' }, { code: 'RO-VS', countryCode: 'RO', name: 'Vaslui' }, { code: 'RO-VN', countryCode: 'RO', name: 'Vrancea' }, { code: 'RS-00', countryCode: 'RS', name: 'Beograd' }, { code: 'RS-14', countryCode: 'RS', name: 'Borski okrug' }, { code: 'RS-11', countryCode: 'RS', name: 'Branicevski okrug' }, { code: 'RS-23', countryCode: 'RS', name: 'Jablanicki okrug' }, { code: 'RS-06', countryCode: 'RS', name: 'Juznobacki okrug' }, { code: 'RS-04', countryCode: 'RS', name: 'Juznobanatski okrug' }, { code: 'RS-09', countryCode: 'RS', name: 'Kolubarski okrug' }, { code: 'RS-28', countryCode: 'RS', name: 'Kosovsko-Mitrovacki okrug' }, { code: 'RS-08', countryCode: 'RS', name: 'Macvanski okrug' }, { code: 'RS-17', countryCode: 'RS', name: 'Moravicki okrug' }, { code: 'RS-20', countryCode: 'RS', name: 'Nisavski okrug' }, { code: 'RS-24', countryCode: 'RS', name: 'Pcinjski okrug' }, { code: 'RS-26', countryCode: 'RS', name: 'Pecki okrug' }, { code: 'RS-22', countryCode: 'RS', name: 'Pirotski okrug' }, { code: 'RS-10', countryCode: 'RS', name: 'Podunavski okrug' }, { code: 'RS-27', countryCode: 'RS', name: 'Prizrenski okrug' }, { code: 'RS-19', countryCode: 'RS', name: 'Rasinski okrug' }, { code: 'RS-18', countryCode: 'RS', name: 'Raski okrug' }, { code: 'RS-01', countryCode: 'RS', name: 'Severnobacki okrug' }, { code: 'RS-03', countryCode: 'RS', name: 'Severnobanatski okrug' }, { code: 'RS-02', countryCode: 'RS', name: 'Srednjebanatski okrug' }, { code: 'RS-07', countryCode: 'RS', name: 'Sremski okrug' }, { code: 'RS-12', countryCode: 'RS', name: 'Sumadijski okrug' }, { code: 'RS-21', countryCode: 'RS', name: 'Toplicki okrug' }, { code: 'RS-15', countryCode: 'RS', name: 'Zajecarski okrug' }, { code: 'RS-05', countryCode: 'RS', name: 'Zapadnobacki okrug' }, { code: 'RS-16', countryCode: 'RS', name: 'Zlatiborski okrug' }, { code: 'RU-AD', countryCode: 'RU', name: 'Adygeya, Respublika' }, { code: 'RU-AL', countryCode: 'RU', name: 'Altay, Respublika' }, { code: 'RU-ALT', countryCode: 'RU', name: 'Altayskiy kray' }, { code: 'RU-AMU', countryCode: 'RU', name: "Amurskaya oblast'" }, { code: 'RU-ARK', countryCode: 'RU', name: "Arkhangel'skaya oblast'" }, { code: 'RU-AST', countryCode: 'RU', name: "Astrakhanskaya oblast'" }, { code: 'RU-BA', countryCode: 'RU', name: 'Bashkortostan, Respublika' }, { code: 'RU-BEL', countryCode: 'RU', name: "Belgorodskaya oblast'" }, { code: 'RU-BRY', countryCode: 'RU', name: "Bryanskaya oblast'" }, { code: 'RU-BU', countryCode: 'RU', name: 'Buryatiya, Respublika' }, { code: 'RU-CE', countryCode: 'RU', name: 'Chechenskaya Respublika' }, { code: 'RU-CHE', countryCode: 'RU', name: "Chelyabinskaya oblast'" }, { code: 'RU-CHU', countryCode: 'RU', name: 'Chukotskiy avtonomnyy okrug' }, { code: 'RU-CU', countryCode: 'RU', name: 'Chuvashskaya Respublika' }, { code: 'RU-DA', countryCode: 'RU', name: 'Dagestan, Respublika' }, { code: 'RU-IN', countryCode: 'RU', name: 'Ingushetiya, Respublika' }, { code: 'RU-IRK', countryCode: 'RU', name: "Irkutskaya oblast'" }, { code: 'RU-IVA', countryCode: 'RU', name: "Ivanovskaya oblast'" }, { code: 'RU-KB', countryCode: 'RU', name: 'Kabardino-Balkarskaya Respublika' }, { code: 'RU-KGD', countryCode: 'RU', name: "Kaliningradskaya oblast'" }, { code: 'RU-KL', countryCode: 'RU', name: 'Kalmykiya, Respublika' }, { code: 'RU-KLU', countryCode: 'RU', name: "Kaluzhskaya oblast'" }, { code: 'RU-KAM', countryCode: 'RU', name: 'Kamchatskiy kray' }, { code: 'RU-KC', countryCode: 'RU', name: 'Karachayevo-Cherkesskaya Respublika' }, { code: 'RU-KR', countryCode: 'RU', name: 'Kareliya, Respublika' }, { code: 'RU-KEM', countryCode: 'RU', name: "Kemerovskaya oblast'" }, { code: 'RU-KHA', countryCode: 'RU', name: 'Khabarovskiy kray' }, { code: 'RU-KK', countryCode: 'RU', name: 'Khakasiya, Respublika' }, { code: 'RU-KHM', countryCode: 'RU', name: 'Khanty-Mansiyskiy avtonomnyy okrug' }, { code: 'RU-KIR', countryCode: 'RU', name: "Kirovskaya oblast'" }, { code: 'RU-KO', countryCode: 'RU', name: 'Komi, Respublika' }, { code: 'RU-KOS', countryCode: 'RU', name: "Kostromskaya oblast'" }, { code: 'RU-KDA', countryCode: 'RU', name: 'Krasnodarskiy kray' }, { code: 'RU-KYA', countryCode: 'RU', name: 'Krasnoyarskiy kray' }, { code: 'RU-KGN', countryCode: 'RU', name: "Kurganskaya oblast'" }, { code: 'RU-KRS', countryCode: 'RU', name: "Kurskaya oblast'" }, { code: 'RU-LEN', countryCode: 'RU', name: "Leningradskaya oblast'" }, { code: 'RU-LIP', countryCode: 'RU', name: "Lipetskaya oblast'" }, { code: 'RU-MAG', countryCode: 'RU', name: "Magadanskaya oblast'" }, { code: 'RU-ME', countryCode: 'RU', name: 'Mariy El, Respublika' }, { code: 'RU-MO', countryCode: 'RU', name: 'Mordoviya, Respublika' }, { code: 'RU-MOS', countryCode: 'RU', name: "Moskovskaya oblast'" }, { code: 'RU-MOW', countryCode: 'RU', name: 'Moskva' }, { code: 'RU-MUR', countryCode: 'RU', name: "Murmanskaya oblast'" }, { code: 'RU-NEN', countryCode: 'RU', name: 'Nenetskiy avtonomnyy okrug' }, { code: 'RU-NIZ', countryCode: 'RU', name: "Nizhegorodskaya oblast'" }, { code: 'RU-NGR', countryCode: 'RU', name: "Novgorodskaya oblast'" }, { code: 'RU-NVS', countryCode: 'RU', name: "Novosibirskaya oblast'" }, { code: 'RU-OMS', countryCode: 'RU', name: "Omskaya oblast'" }, { code: 'RU-ORE', countryCode: 'RU', name: "Orenburgskaya oblast'" }, { code: 'RU-ORL', countryCode: 'RU', name: "Orlovskaya oblast'" }, { code: 'RU-PNZ', countryCode: 'RU', name: "Penzenskaya oblast'" }, { code: 'RU-PER', countryCode: 'RU', name: 'Permskiy kray' }, { code: 'RU-PRI', countryCode: 'RU', name: 'Primorskiy kray' }, { code: 'RU-PSK', countryCode: 'RU', name: "Pskovskaya oblast'" }, { code: 'RU-ROS', countryCode: 'RU', name: "Rostovskaya oblast'" }, { code: 'RU-RYA', countryCode: 'RU', name: "Ryazanskaya oblast'" }, { code: 'RU-SA', countryCode: 'RU', name: 'Saha, Respublika' }, { code: 'RU-SAK', countryCode: 'RU', name: "Sakhalinskaya oblast'" }, { code: 'RU-SAM', countryCode: 'RU', name: "Samarskaya oblast'" }, { code: 'RU-SPE', countryCode: 'RU', name: 'Sankt-Peterburg' }, { code: 'RU-SAR', countryCode: 'RU', name: "Saratovskaya oblast'" }, { code: 'RU-SE', countryCode: 'RU', name: 'Severnaya Osetiya, Respublika' }, { code: 'RU-SMO', countryCode: 'RU', name: "Smolenskaya oblast'" }, { code: 'RU-STA', countryCode: 'RU', name: "Stavropol'skiy kray" }, { code: 'RU-SVE', countryCode: 'RU', name: "Sverdlovskaya oblast'" }, { code: 'RU-TAM', countryCode: 'RU', name: "Tambovskaya oblast'" }, { code: 'RU-TA', countryCode: 'RU', name: 'Tatarstan, Respublika' }, { code: 'RU-TOM', countryCode: 'RU', name: "Tomskaya oblast'" }, { code: 'RU-TUL', countryCode: 'RU', name: "Tul'skaya oblast'" }, { code: 'RU-TVE', countryCode: 'RU', name: "Tverskaya oblast'" }, { code: 'RU-TYU', countryCode: 'RU', name: "Tyumenskaya oblast'" }, { code: 'RU-TY', countryCode: 'RU', name: 'Tyva, Respublika' }, { code: 'RU-UD', countryCode: 'RU', name: 'Udmurtskaya Respublika' }, { code: 'RU-ULY', countryCode: 'RU', name: "Ul'yanovskaya oblast'" }, { code: 'RU-VLA', countryCode: 'RU', name: "Vladimirskaya oblast'" }, { code: 'RU-VGG', countryCode: 'RU', name: "Volgogradskaya oblast'" }, { code: 'RU-VLG', countryCode: 'RU', name: "Vologodskaya oblast'" }, { code: 'RU-VOR', countryCode: 'RU', name: "Voronezhskaya oblast'" }, { code: 'RU-YAN', countryCode: 'RU', name: 'Yamalo-Nenetskiy avtonomnyy okrug' }, { code: 'RU-YAR', countryCode: 'RU', name: "Yaroslavskaya oblast'" }, { code: 'RU-YEV', countryCode: 'RU', name: "Yevreyskaya avtonomnaya oblast'" }, { code: 'RU-ZAB', countryCode: 'RU', name: "Zabaykal'skiy kray" }, { code: 'RW-02', countryCode: 'RW', name: 'Est' }, { code: 'RW-03', countryCode: 'RW', name: 'Nord' }, { code: 'RW-04', countryCode: 'RW', name: 'Ouest' }, { code: 'RW-05', countryCode: 'RW', name: 'Sud' }, { code: 'RW-01', countryCode: 'RW', name: 'Ville de Kigali' }, { code: 'SA-14', countryCode: 'SA', name: "'Asir" }, { code: 'SA-11', countryCode: 'SA', name: 'Al Bahah' }, { code: 'SA-08', countryCode: 'SA', name: 'Al Hudud ash Shamaliyah' }, { code: 'SA-12', countryCode: 'SA', name: 'Al Jawf' }, { code: 'SA-03', countryCode: 'SA', name: 'Al Madinah al Munawwarah' }, { code: 'SA-05', countryCode: 'SA', name: 'Al Qasim' }, { code: 'SA-01', countryCode: 'SA', name: 'Ar Riyad' }, { code: 'SA-04', countryCode: 'SA', name: 'Ash Sharqiyah' }, { code: 'SA-06', countryCode: 'SA', name: "Ha'il" }, { code: 'SA-09', countryCode: 'SA', name: 'Jazan' }, { code: 'SA-02', countryCode: 'SA', name: 'Makkah al Mukarramah' }, { code: 'SA-10', countryCode: 'SA', name: 'Najran' }, { code: 'SA-07', countryCode: 'SA', name: 'Tabuk' }, { code: 'SB-CE', countryCode: 'SB', name: 'Central' }, { code: 'SB-GU', countryCode: 'SB', name: 'Guadalcanal' }, { code: 'SB-IS', countryCode: 'SB', name: 'Isabel' }, { code: 'SB-MK', countryCode: 'SB', name: 'Makira-Ulawa' }, { code: 'SB-ML', countryCode: 'SB', name: 'Malaita' }, { code: 'SB-WE', countryCode: 'SB', name: 'Western' }, { code: 'SC-16', countryCode: 'SC', name: 'English River' }, { code: 'SD-NB', countryCode: 'SD', name: 'Blue Nile' }, { code: 'SD-GD', countryCode: 'SD', name: 'Gedaref' }, { code: 'SD-GZ', countryCode: 'SD', name: 'Gezira' }, { code: 'SD-KA', countryCode: 'SD', name: 'Kassala' }, { code: 'SD-KH', countryCode: 'SD', name: 'Khartoum' }, { code: 'SD-DN', countryCode: 'SD', name: 'North Darfur' }, { code: 'SD-KN', countryCode: 'SD', name: 'North Kordofan' }, { code: 'SD-NO', countryCode: 'SD', name: 'Northern' }, { code: 'SD-RS', countryCode: 'SD', name: 'Red Sea' }, { code: 'SD-NR', countryCode: 'SD', name: 'River Nile' }, { code: 'SD-SI', countryCode: 'SD', name: 'Sennar' }, { code: 'SD-DS', countryCode: 'SD', name: 'South Darfur' }, { code: 'SD-KS', countryCode: 'SD', name: 'South Kordofan' }, { code: 'SD-DW', countryCode: 'SD', name: 'West Darfur' }, { code: 'SD-NW', countryCode: 'SD', name: 'White Nile' }, { code: 'SE-K', countryCode: 'SE', name: 'Blekinge lan' }, { code: 'SE-W', countryCode: 'SE', name: 'Dalarnas lan' }, { code: 'SE-X', countryCode: 'SE', name: 'Gavleborgs lan' }, { code: 'SE-I', countryCode: 'SE', name: 'Gotlands lan' }, { code: 'SE-N', countryCode: 'SE', name: 'Hallands lan' }, { code: 'SE-Z', countryCode: 'SE', name: 'Jamtlands lan' }, { code: 'SE-F', countryCode: 'SE', name: 'Jonkopings lan' }, { code: 'SE-H', countryCode: 'SE', name: 'Kalmar lan' }, { code: 'SE-G', countryCode: 'SE', name: 'Kronobergs lan' }, { code: 'SE-BD', countryCode: 'SE', name: 'Norrbottens lan' }, { code: 'SE-T', countryCode: 'SE', name: 'Orebro lan' }, { code: 'SE-E', countryCode: 'SE', name: 'Ostergotlands lan' }, { code: 'SE-M', countryCode: 'SE', name: 'Skane lan' }, { code: 'SE-D', countryCode: 'SE', name: 'Sodermanlands lan' }, { code: 'SE-AB', countryCode: 'SE', name: 'Stockholms lan' }, { code: 'SE-C', countryCode: 'SE', name: 'Uppsala lan' }, { code: 'SE-S', countryCode: 'SE', name: 'Varmlands lan' }, { code: 'SE-AC', countryCode: 'SE', name: 'Vasterbottens lan' }, { code: 'SE-Y', countryCode: 'SE', name: 'Vasternorrlands lan' }, { code: 'SE-U', countryCode: 'SE', name: 'Vastmanlands lan' }, { code: 'SE-O', countryCode: 'SE', name: 'Vastra Gotalands lan' }, { code: 'SH-AC', countryCode: 'SH', name: 'Ascension' }, { code: 'SH-HL', countryCode: 'SH', name: 'Saint Helena' }, { code: 'SH-TA', countryCode: 'SH', name: 'Tristan da Cunha' }, { code: 'SI-001', countryCode: 'SI', name: 'Ajdovscina' }, { code: 'SI-003', countryCode: 'SI', name: 'Bled' }, { code: 'SI-004', countryCode: 'SI', name: 'Bohinj' }, { code: 'SI-005', countryCode: 'SI', name: 'Borovnica' }, { code: 'SI-006', countryCode: 'SI', name: 'Bovec' }, { code: 'SI-009', countryCode: 'SI', name: 'Brezice' }, { code: 'SI-008', countryCode: 'SI', name: 'Brezovica' }, { code: 'SI-011', countryCode: 'SI', name: 'Celje' }, { code: 'SI-013', countryCode: 'SI', name: 'Cerknica' }, { code: 'SI-014', countryCode: 'SI', name: 'Cerkno' }, { code: 'SI-015', countryCode: 'SI', name: 'Crensovci' }, { code: 'SI-017', countryCode: 'SI', name: 'Crnomelj' }, { code: 'SI-018', countryCode: 'SI', name: 'Destrnik' }, { code: 'SI-019', countryCode: 'SI', name: 'Divaca' }, { code: 'SI-023', countryCode: 'SI', name: 'Domzale' }, { code: 'SI-025', countryCode: 'SI', name: 'Dravograd' }, { code: 'SI-029', countryCode: 'SI', name: 'Gornja Radgona' }, { code: 'SI-032', countryCode: 'SI', name: 'Grosuplje' }, { code: 'SI-160', countryCode: 'SI', name: 'Hoce-Slivnica' }, { code: 'SI-162', countryCode: 'SI', name: 'Horjul' }, { code: 'SI-034', countryCode: 'SI', name: 'Hrastnik' }, { code: 'SI-036', countryCode: 'SI', name: 'Idrija' }, { code: 'SI-037', countryCode: 'SI', name: 'Ig' }, { code: 'SI-038', countryCode: 'SI', name: 'Ilirska Bistrica' }, { code: 'SI-039', countryCode: 'SI', name: 'Ivancna Gorica' }, { code: 'SI-040', countryCode: 'SI', name: 'Izola' }, { code: 'SI-041', countryCode: 'SI', name: 'Jesenice' }, { code: 'SI-043', countryCode: 'SI', name: 'Kamnik' }, { code: 'SI-044', countryCode: 'SI', name: 'Kanal' }, { code: 'SI-045', countryCode: 'SI', name: 'Kidricevo' }, { code: 'SI-046', countryCode: 'SI', name: 'Kobarid' }, { code: 'SI-048', countryCode: 'SI', name: 'Kocevje' }, { code: 'SI-050', countryCode: 'SI', name: 'Koper' }, { code: 'SI-052', countryCode: 'SI', name: 'Kranj' }, { code: 'SI-053', countryCode: 'SI', name: 'Kranjska Gora' }, { code: 'SI-054', countryCode: 'SI', name: 'Krsko' }, { code: 'SI-057', countryCode: 'SI', name: 'Lasko' }, { code: 'SI-058', countryCode: 'SI', name: 'Lenart' }, { code: 'SI-059', countryCode: 'SI', name: 'Lendava' }, { code: 'SI-060', countryCode: 'SI', name: 'Litija' }, { code: 'SI-061', countryCode: 'SI', name: 'Ljubljana' }, { code: 'SI-063', countryCode: 'SI', name: 'Ljutomer' }, { code: 'SI-208', countryCode: 'SI', name: 'Log-Dragomer' }, { code: 'SI-064', countryCode: 'SI', name: 'Logatec' }, { code: 'SI-167', countryCode: 'SI', name: 'Lovrenc na Pohorju' }, { code: 'SI-070', countryCode: 'SI', name: 'Maribor' }, { code: 'SI-071', countryCode: 'SI', name: 'Medvode' }, { code: 'SI-072', countryCode: 'SI', name: 'Menges' }, { code: 'SI-073', countryCode: 'SI', name: 'Metlika' }, { code: 'SI-074', countryCode: 'SI', name: 'Mezica' }, { code: 'SI-169', countryCode: 'SI', name: 'Miklavz na Dravskem Polju' }, { code: 'SI-075', countryCode: 'SI', name: 'Miren-Kostanjevica' }, { code: 'SI-076', countryCode: 'SI', name: 'Mislinja' }, { code: 'SI-079', countryCode: 'SI', name: 'Mozirje' }, { code: 'SI-080', countryCode: 'SI', name: 'Murska Sobota' }, { code: 'SI-081', countryCode: 'SI', name: 'Muta' }, { code: 'SI-084', countryCode: 'SI', name: 'Nova Gorica' }, { code: 'SI-085', countryCode: 'SI', name: 'Novo Mesto' }, { code: 'SI-086', countryCode: 'SI', name: 'Odranci' }, { code: 'SI-171', countryCode: 'SI', name: 'Oplotnica' }, { code: 'SI-087', countryCode: 'SI', name: 'Ormoz' }, { code: 'SI-090', countryCode: 'SI', name: 'Piran' }, { code: 'SI-091', countryCode: 'SI', name: 'Pivka' }, { code: 'SI-200', countryCode: 'SI', name: 'Poljcane' }, { code: 'SI-173', countryCode: 'SI', name: 'Polzela' }, { code: 'SI-094', countryCode: 'SI', name: 'Postojna' }, { code: 'SI-174', countryCode: 'SI', name: 'Prebold' }, { code: 'SI-175', countryCode: 'SI', name: 'Prevalje' }, { code: 'SI-096', countryCode: 'SI', name: 'Ptuj' }, { code: 'SI-098', countryCode: 'SI', name: 'Race-Fram' }, { code: 'SI-099', countryCode: 'SI', name: 'Radece' }, { code: 'SI-100', countryCode: 'SI', name: 'Radenci' }, { code: 'SI-101', countryCode: 'SI', name: 'Radlje ob Dravi' }, { code: 'SI-102', countryCode: 'SI', name: 'Radovljica' }, { code: 'SI-103', countryCode: 'SI', name: 'Ravne na Koroskem' }, { code: 'SI-104', countryCode: 'SI', name: 'Ribnica' }, { code: 'SI-106', countryCode: 'SI', name: 'Rogaska Slatina' }, { code: 'SI-108', countryCode: 'SI', name: 'Ruse' }, { code: 'SI-183', countryCode: 'SI', name: 'Sempeter-Vrtojba' }, { code: 'SI-117', countryCode: 'SI', name: 'Sencur' }, { code: 'SI-118', countryCode: 'SI', name: 'Sentilj' }, { code: 'SI-120', countryCode: 'SI', name: 'Sentjur' }, { code: 'SI-110', countryCode: 'SI', name: 'Sevnica' }, { code: 'SI-111', countryCode: 'SI', name: 'Sezana' }, { code: 'SI-122', countryCode: 'SI', name: 'Skofja Loka' }, { code: 'SI-123', countryCode: 'SI', name: 'Skofljica' }, { code: 'SI-112', countryCode: 'SI', name: 'Slovenj Gradec' }, { code: 'SI-113', countryCode: 'SI', name: 'Slovenska Bistrica' }, { code: 'SI-114', countryCode: 'SI', name: 'Slovenske Konjice' }, { code: 'SI-126', countryCode: 'SI', name: 'Sostanj' }, { code: 'SI-127', countryCode: 'SI', name: 'Store' }, { code: 'SI-203', countryCode: 'SI', name: 'Straza' }, { code: 'SI-128', countryCode: 'SI', name: 'Tolmin' }, { code: 'SI-129', countryCode: 'SI', name: 'Trbovlje' }, { code: 'SI-130', countryCode: 'SI', name: 'Trebnje' }, { code: 'SI-131', countryCode: 'SI', name: 'Trzic' }, { code: 'SI-186', countryCode: 'SI', name: 'Trzin' }, { code: 'SI-132', countryCode: 'SI', name: 'Turnisce' }, { code: 'SI-133', countryCode: 'SI', name: 'Velenje' }, { code: 'SI-136', countryCode: 'SI', name: 'Vipava' }, { code: 'SI-138', countryCode: 'SI', name: 'Vodice' }, { code: 'SI-139', countryCode: 'SI', name: 'Vojnik' }, { code: 'SI-140', countryCode: 'SI', name: 'Vrhnika' }, { code: 'SI-141', countryCode: 'SI', name: 'Vuzenica' }, { code: 'SI-142', countryCode: 'SI', name: 'Zagorje ob Savi' }, { code: 'SI-190', countryCode: 'SI', name: 'Zalec' }, { code: 'SI-146', countryCode: 'SI', name: 'Zelezniki' }, { code: 'SI-147', countryCode: 'SI', name: 'Ziri' }, { code: 'SI-144', countryCode: 'SI', name: 'Zrece' }, { code: 'SI-193', countryCode: 'SI', name: 'Zuzemberk' }, { code: 'SK-BC', countryCode: 'SK', name: 'Banskobystricky kraj' }, { code: 'SK-BL', countryCode: 'SK', name: 'Bratislavsky kraj' }, { code: 'SK-KI', countryCode: 'SK', name: 'Kosicky kraj' }, { code: 'SK-NI', countryCode: 'SK', name: 'Nitriansky kraj' }, { code: 'SK-PV', countryCode: 'SK', name: 'Presovsky kraj' }, { code: 'SK-TC', countryCode: 'SK', name: 'Trenciansky kraj' }, { code: 'SK-TA', countryCode: 'SK', name: 'Trnavsky kraj' }, { code: 'SK-ZI', countryCode: 'SK', name: 'Zilinsky kraj' }, { code: 'SL-E', countryCode: 'SL', name: 'Eastern' }, { code: 'SL-N', countryCode: 'SL', name: 'Northern' }, { code: 'SL-S', countryCode: 'SL', name: 'Southern' }, { code: 'SL-W', countryCode: 'SL', name: 'Western Area' }, { code: 'SM-01', countryCode: 'SM', name: 'Acquaviva' }, { code: 'SM-02', countryCode: 'SM', name: 'Chiesanuova' }, { code: 'SM-07', countryCode: 'SM', name: 'San Marino' }, { code: 'SM-09', countryCode: 'SM', name: 'Serravalle' }, { code: 'SN-DK', countryCode: 'SN', name: 'Dakar' }, { code: 'SN-DB', countryCode: 'SN', name: 'Diourbel' }, { code: 'SN-FK', countryCode: 'SN', name: 'Fatick' }, { code: 'SN-KA', countryCode: 'SN', name: 'Kaffrine' }, { code: 'SN-KL', countryCode: 'SN', name: 'Kaolack' }, { code: 'SN-KE', countryCode: 'SN', name: 'Kedougou' }, { code: 'SN-KD', countryCode: 'SN', name: 'Kolda' }, { code: 'SN-LG', countryCode: 'SN', name: 'Louga' }, { code: 'SN-MT', countryCode: 'SN', name: 'Matam' }, { code: 'SN-SL', countryCode: 'SN', name: 'Saint-Louis' }, { code: 'SN-SE', countryCode: 'SN', name: 'Sedhiou' }, { code: 'SN-TC', countryCode: 'SN', name: 'Tambacounda' }, { code: 'SN-TH', countryCode: 'SN', name: 'Thies' }, { code: 'SN-ZG', countryCode: 'SN', name: 'Ziguinchor' }, { code: 'SO-AW', countryCode: 'SO', name: 'Awdal' }, { code: 'SO-BK', countryCode: 'SO', name: 'Bakool' }, { code: 'SO-BN', countryCode: 'SO', name: 'Banaadir' }, { code: 'SO-BR', countryCode: 'SO', name: 'Bari' }, { code: 'SO-BY', countryCode: 'SO', name: 'Bay' }, { code: 'SO-GA', countryCode: 'SO', name: 'Galguduud' }, { code: 'SO-GE', countryCode: 'SO', name: 'Gedo' }, { code: 'SO-HI', countryCode: 'SO', name: 'Hiiraan' }, { code: 'SO-JD', countryCode: 'SO', name: 'Jubbada Dhexe' }, { code: 'SO-JH', countryCode: 'SO', name: 'Jubbada Hoose' }, { code: 'SO-MU', countryCode: 'SO', name: 'Mudug' }, { code: 'SO-NU', countryCode: 'SO', name: 'Nugaal' }, { code: 'SO-SA', countryCode: 'SO', name: 'Sanaag' }, { code: 'SO-SD', countryCode: 'SO', name: 'Shabeellaha Dhexe' }, { code: 'SO-SH', countryCode: 'SO', name: 'Shabeellaha Hoose' }, { code: 'SO-SO', countryCode: 'SO', name: 'Sool' }, { code: 'SO-TO', countryCode: 'SO', name: 'Togdheer' }, { code: 'SO-WO', countryCode: 'SO', name: 'Woqooyi Galbeed' }, { code: 'SR-BR', countryCode: 'SR', name: 'Brokopondo' }, { code: 'SR-CM', countryCode: 'SR', name: 'Commewijne' }, { code: 'SR-CR', countryCode: 'SR', name: 'Coronie' }, { code: 'SR-MA', countryCode: 'SR', name: 'Marowijne' }, { code: 'SR-NI', countryCode: 'SR', name: 'Nickerie' }, { code: 'SR-PR', countryCode: 'SR', name: 'Para' }, { code: 'SR-PM', countryCode: 'SR', name: 'Paramaribo' }, { code: 'SR-SA', countryCode: 'SR', name: 'Saramacca' }, { code: 'SR-WA', countryCode: 'SR', name: 'Wanica' }, { code: 'SS-EC', countryCode: 'SS', name: 'Central Equatoria' }, { code: 'SS-EE', countryCode: 'SS', name: 'Eastern Equatoria' }, { code: 'SS-JG', countryCode: 'SS', name: 'Jonglei' }, { code: 'SS-LK', countryCode: 'SS', name: 'Lakes' }, { code: 'SS-BN', countryCode: 'SS', name: 'Northern Bahr el Ghazal' }, { code: 'SS-UY', countryCode: 'SS', name: 'Unity' }, { code: 'SS-NU', countryCode: 'SS', name: 'Upper Nile' }, { code: 'SS-WR', countryCode: 'SS', name: 'Warrap' }, { code: 'SS-BW', countryCode: 'SS', name: 'Western Bahr el Ghazal' }, { code: 'SS-EW', countryCode: 'SS', name: 'Western Equatoria' }, { code: 'ST-P', countryCode: 'ST', name: 'Principe' }, { code: 'ST-S', countryCode: 'ST', name: 'Sao Tome' }, { code: 'SV-AH', countryCode: 'SV', name: 'Ahuachapan' }, { code: 'SV-CA', countryCode: 'SV', name: 'Cabanas' }, { code: 'SV-CH', countryCode: 'SV', name: 'Chalatenango' }, { code: 'SV-CU', countryCode: 'SV', name: 'Cuscatlan' }, { code: 'SV-LI', countryCode: 'SV', name: 'La Libertad' }, { code: 'SV-PA', countryCode: 'SV', name: 'La Paz' }, { code: 'SV-UN', countryCode: 'SV', name: 'La Union' }, { code: 'SV-MO', countryCode: 'SV', name: 'Morazan' }, { code: 'SV-SM', countryCode: 'SV', name: 'San Miguel' }, { code: 'SV-SS', countryCode: 'SV', name: 'San Salvador' }, { code: 'SV-SV', countryCode: 'SV', name: 'San Vicente' }, { code: 'SV-SA', countryCode: 'SV', name: 'Santa Ana' }, { code: 'SV-SO', countryCode: 'SV', name: 'Sonsonate' }, { code: 'SV-US', countryCode: 'SV', name: 'Usulutan' }, { code: 'SY-HA', countryCode: 'SY', name: 'Al Hasakah' }, { code: 'SY-LA', countryCode: 'SY', name: 'Al Ladhiqiyah' }, { code: 'SY-QU', countryCode: 'SY', name: 'Al Qunaytirah' }, { code: 'SY-RA', countryCode: 'SY', name: 'Ar Raqqah' }, { code: 'SY-SU', countryCode: 'SY', name: "As Suwayda'" }, { code: 'SY-DR', countryCode: 'SY', name: "Dar'a" }, { code: 'SY-DY', countryCode: 'SY', name: 'Dayr az Zawr' }, { code: 'SY-DI', countryCode: 'SY', name: 'Dimashq' }, { code: 'SY-HL', countryCode: 'SY', name: 'Halab' }, { code: 'SY-HM', countryCode: 'SY', name: 'Hamah' }, { code: 'SY-HI', countryCode: 'SY', name: 'Hims' }, { code: 'SY-ID', countryCode: 'SY', name: 'Idlib' }, { code: 'SY-RD', countryCode: 'SY', name: 'Rif Dimashq' }, { code: 'SY-TA', countryCode: 'SY', name: 'Tartus' }, { code: 'SZ-HH', countryCode: 'SZ', name: 'Hhohho' }, { code: 'SZ-LU', countryCode: 'SZ', name: 'Lubombo' }, { code: 'SZ-MA', countryCode: 'SZ', name: 'Manzini' }, { code: 'SZ-SH', countryCode: 'SZ', name: 'Shiselweni' }, { code: 'TD-BG', countryCode: 'TD', name: 'Bahr el Gazel' }, { code: 'TD-BA', countryCode: 'TD', name: 'Batha' }, { code: 'TD-BO', countryCode: 'TD', name: 'Borkou' }, { code: 'TD-CB', countryCode: 'TD', name: 'Chari-Baguirmi' }, { code: 'TD-GR', countryCode: 'TD', name: 'Guera' }, { code: 'TD-HL', countryCode: 'TD', name: 'Hadjer Lamis' }, { code: 'TD-KA', countryCode: 'TD', name: 'Kanem' }, { code: 'TD-LC', countryCode: 'TD', name: 'Lac' }, { code: 'TD-LO', countryCode: 'TD', name: 'Logone-Occidental' }, { code: 'TD-LR', countryCode: 'TD', name: 'Logone-Oriental' }, { code: 'TD-MA', countryCode: 'TD', name: 'Mandoul' }, { code: 'TD-ME', countryCode: 'TD', name: 'Mayo-Kebbi-Est' }, { code: 'TD-MO', countryCode: 'TD', name: 'Mayo-Kebbi-Ouest' }, { code: 'TD-MC', countryCode: 'TD', name: 'Moyen-Chari' }, { code: 'TD-OD', countryCode: 'TD', name: 'Ouaddai' }, { code: 'TD-SA', countryCode: 'TD', name: 'Salamat' }, { code: 'TD-TA', countryCode: 'TD', name: 'Tandjile' }, { code: 'TD-TI', countryCode: 'TD', name: 'Tibesti' }, { code: 'TD-WF', countryCode: 'TD', name: 'Wadi Fira' }, { code: 'TG-C', countryCode: 'TG', name: 'Centrale' }, { code: 'TG-K', countryCode: 'TG', name: 'Kara' }, { code: 'TG-M', countryCode: 'TG', name: 'Maritime' }, { code: 'TG-P', countryCode: 'TG', name: 'Plateaux' }, { code: 'TG-S', countryCode: 'TG', name: 'Savannes' }, { code: 'TH-37', countryCode: 'TH', name: 'Amnat Charoen' }, { code: 'TH-15', countryCode: 'TH', name: 'Ang Thong' }, { code: 'TH-31', countryCode: 'TH', name: 'Buri Ram' }, { code: 'TH-24', countryCode: 'TH', name: 'Chachoengsao' }, { code: 'TH-18', countryCode: 'TH', name: 'Chai Nat' }, { code: 'TH-36', countryCode: 'TH', name: 'Chaiyaphum' }, { code: 'TH-22', countryCode: 'TH', name: 'Chanthaburi' }, { code: 'TH-50', countryCode: 'TH', name: 'Chiang Mai' }, { code: 'TH-57', countryCode: 'TH', name: 'Chiang Rai' }, { code: 'TH-20', countryCode: 'TH', name: 'Chon Buri' }, { code: 'TH-86', countryCode: 'TH', name: 'Chumphon' }, { code: 'TH-46', countryCode: 'TH', name: 'Kalasin' }, { code: 'TH-62', countryCode: 'TH', name: 'Kamphaeng Phet' }, { code: 'TH-71', countryCode: 'TH', name: 'Kanchanaburi' }, { code: 'TH-40', countryCode: 'TH', name: 'Khon Kaen' }, { code: 'TH-81', countryCode: 'TH', name: 'Krabi' }, { code: 'TH-10', countryCode: 'TH', name: 'Krung Thep Maha Nakhon' }, { code: 'TH-52', countryCode: 'TH', name: 'Lampang' }, { code: 'TH-51', countryCode: 'TH', name: 'Lamphun' }, { code: 'TH-42', countryCode: 'TH', name: 'Loei' }, { code: 'TH-16', countryCode: 'TH', name: 'Lop Buri' }, { code: 'TH-58', countryCode: 'TH', name: 'Mae Hong Son' }, { code: 'TH-44', countryCode: 'TH', name: 'Maha Sarakham' }, { code: 'TH-49', countryCode: 'TH', name: 'Mukdahan' }, { code: 'TH-26', countryCode: 'TH', name: 'Nakhon Nayok' }, { code: 'TH-73', countryCode: 'TH', name: 'Nakhon Pathom' }, { code: 'TH-48', countryCode: 'TH', name: 'Nakhon Phanom' }, { code: 'TH-30', countryCode: 'TH', name: 'Nakhon Ratchasima' }, { code: 'TH-60', countryCode: 'TH', name: 'Nakhon Sawan' }, { code: 'TH-80', countryCode: 'TH', name: 'Nakhon Si Thammarat' }, { code: 'TH-55', countryCode: 'TH', name: 'Nan' }, { code: 'TH-96', countryCode: 'TH', name: 'Narathiwat' }, { code: 'TH-39', countryCode: 'TH', name: 'Nong Bua Lam Phu' }, { code: 'TH-43', countryCode: 'TH', name: 'Nong Khai' }, { code: 'TH-12', countryCode: 'TH', name: 'Nonthaburi' }, { code: 'TH-13', countryCode: 'TH', name: 'Pathum Thani' }, { code: 'TH-94', countryCode: 'TH', name: 'Pattani' }, { code: 'TH-82', countryCode: 'TH', name: 'Phangnga' }, { code: 'TH-93', countryCode: 'TH', name: 'Phatthalung' }, { code: 'TH-56', countryCode: 'TH', name: 'Phayao' }, { code: 'TH-67', countryCode: 'TH', name: 'Phetchabun' }, { code: 'TH-76', countryCode: 'TH', name: 'Phetchaburi' }, { code: 'TH-66', countryCode: 'TH', name: 'Phichit' }, { code: 'TH-65', countryCode: 'TH', name: 'Phitsanulok' }, { code: 'TH-14', countryCode: 'TH', name: 'Phra Nakhon Si Ayutthaya' }, { code: 'TH-54', countryCode: 'TH', name: 'Phrae' }, { code: 'TH-83', countryCode: 'TH', name: 'Phuket' }, { code: 'TH-25', countryCode: 'TH', name: 'Prachin Buri' }, { code: 'TH-77', countryCode: 'TH', name: 'Prachuap Khiri Khan' }, { code: 'TH-85', countryCode: 'TH', name: 'Ranong' }, { code: 'TH-70', countryCode: 'TH', name: 'Ratchaburi' }, { code: 'TH-21', countryCode: 'TH', name: 'Rayong' }, { code: 'TH-45', countryCode: 'TH', name: 'Roi Et' }, { code: 'TH-27', countryCode: 'TH', name: 'Sa Kaeo' }, { code: 'TH-47', countryCode: 'TH', name: 'Sakon Nakhon' }, { code: 'TH-11', countryCode: 'TH', name: 'Samut Prakan' }, { code: 'TH-74', countryCode: 'TH', name: 'Samut Sakhon' }, { code: 'TH-75', countryCode: 'TH', name: 'Samut Songkhram' }, { code: 'TH-19', countryCode: 'TH', name: 'Saraburi' }, { code: 'TH-91', countryCode: 'TH', name: 'Satun' }, { code: 'TH-33', countryCode: 'TH', name: 'Si Sa Ket' }, { code: 'TH-17', countryCode: 'TH', name: 'Sing Buri' }, { code: 'TH-90', countryCode: 'TH', name: 'Songkhla' }, { code: 'TH-64', countryCode: 'TH', name: 'Sukhothai' }, { code: 'TH-72', countryCode: 'TH', name: 'Suphan Buri' }, { code: 'TH-84', countryCode: 'TH', name: 'Surat Thani' }, { code: 'TH-32', countryCode: 'TH', name: 'Surin' }, { code: 'TH-63', countryCode: 'TH', name: 'Tak' }, { code: 'TH-92', countryCode: 'TH', name: 'Trang' }, { code: 'TH-23', countryCode: 'TH', name: 'Trat' }, { code: 'TH-34', countryCode: 'TH', name: 'Ubon Ratchathani' }, { code: 'TH-41', countryCode: 'TH', name: 'Udon Thani' }, { code: 'TH-61', countryCode: 'TH', name: 'Uthai Thani' }, { code: 'TH-53', countryCode: 'TH', name: 'Uttaradit' }, { code: 'TH-95', countryCode: 'TH', name: 'Yala' }, { code: 'TH-35', countryCode: 'TH', name: 'Yasothon' }, { code: 'TJ-DU', countryCode: 'TJ', name: 'Dushanbe' }, { code: 'TJ-KT', countryCode: 'TJ', name: 'Khatlon' }, { code: 'TJ-GB', countryCode: 'TJ', name: 'Kuhistoni Badakhshon' }, { code: 'TJ-RA', countryCode: 'TJ', name: 'Nohiyahoi Tobei Jumhuri' }, { code: 'TJ-SU', countryCode: 'TJ', name: 'Sughd' }, { code: 'TL-DI', countryCode: 'TL', name: 'Dili' }, { code: 'TM-A', countryCode: 'TM', name: 'Ahal' }, { code: 'TM-B', countryCode: 'TM', name: 'Balkan' }, { code: 'TM-D', countryCode: 'TM', name: 'Dasoguz' }, { code: 'TM-L', countryCode: 'TM', name: 'Lebap' }, { code: 'TM-M', countryCode: 'TM', name: 'Mary' }, { code: 'TN-31', countryCode: 'TN', name: 'Beja' }, { code: 'TN-13', countryCode: 'TN', name: 'Ben Arous' }, { code: 'TN-23', countryCode: 'TN', name: 'Bizerte' }, { code: 'TN-81', countryCode: 'TN', name: 'Gabes' }, { code: 'TN-71', countryCode: 'TN', name: 'Gafsa' }, { code: 'TN-32', countryCode: 'TN', name: 'Jendouba' }, { code: 'TN-41', countryCode: 'TN', name: 'Kairouan' }, { code: 'TN-42', countryCode: 'TN', name: 'Kasserine' }, { code: 'TN-73', countryCode: 'TN', name: 'Kebili' }, { code: 'TN-12', countryCode: 'TN', name: "L'Ariana" }, { code: 'TN-14', countryCode: 'TN', name: 'La Manouba' }, { code: 'TN-33', countryCode: 'TN', name: 'Le Kef' }, { code: 'TN-53', countryCode: 'TN', name: 'Mahdia' }, { code: 'TN-82', countryCode: 'TN', name: 'Medenine' }, { code: 'TN-52', countryCode: 'TN', name: 'Monastir' }, { code: 'TN-21', countryCode: 'TN', name: 'Nabeul' }, { code: 'TN-61', countryCode: 'TN', name: 'Sfax' }, { code: 'TN-43', countryCode: 'TN', name: 'Sidi Bouzid' }, { code: 'TN-34', countryCode: 'TN', name: 'Siliana' }, { code: 'TN-51', countryCode: 'TN', name: 'Sousse' }, { code: 'TN-83', countryCode: 'TN', name: 'Tataouine' }, { code: 'TN-72', countryCode: 'TN', name: 'Tozeur' }, { code: 'TN-11', countryCode: 'TN', name: 'Tunis' }, { code: 'TN-22', countryCode: 'TN', name: 'Zaghouan' }, { code: 'TO-02', countryCode: 'TO', name: "Ha'apai" }, { code: 'TO-04', countryCode: 'TO', name: 'Tongatapu' }, { code: 'TO-05', countryCode: 'TO', name: "Vava'u" }, { code: 'TR-01', countryCode: 'TR', name: 'Adana' }, { code: 'TR-02', countryCode: 'TR', name: 'Adiyaman' }, { code: 'TR-03', countryCode: 'TR', name: 'Afyonkarahisar' }, { code: 'TR-04', countryCode: 'TR', name: 'Agri' }, { code: 'TR-68', countryCode: 'TR', name: 'Aksaray' }, { code: 'TR-05', countryCode: 'TR', name: 'Amasya' }, { code: 'TR-06', countryCode: 'TR', name: 'Ankara' }, { code: 'TR-07', countryCode: 'TR', name: 'Antalya' }, { code: 'TR-75', countryCode: 'TR', name: 'Ardahan' }, { code: 'TR-08', countryCode: 'TR', name: 'Artvin' }, { code: 'TR-09', countryCode: 'TR', name: 'Aydin' }, { code: 'TR-10', countryCode: 'TR', name: 'Balikesir' }, { code: 'TR-74', countryCode: 'TR', name: 'Bartin' }, { code: 'TR-72', countryCode: 'TR', name: 'Batman' }, { code: 'TR-69', countryCode: 'TR', name: 'Bayburt' }, { code: 'TR-11', countryCode: 'TR', name: 'Bilecik' }, { code: 'TR-12', countryCode: 'TR', name: 'Bingol' }, { code: 'TR-13', countryCode: 'TR', name: 'Bitlis' }, { code: 'TR-14', countryCode: 'TR', name: 'Bolu' }, { code: 'TR-15', countryCode: 'TR', name: 'Burdur' }, { code: 'TR-16', countryCode: 'TR', name: 'Bursa' }, { code: 'TR-17', countryCode: 'TR', name: 'Canakkale' }, { code: 'TR-18', countryCode: 'TR', name: 'Cankiri' }, { code: 'TR-19', countryCode: 'TR', name: 'Corum' }, { code: 'TR-20', countryCode: 'TR', name: 'Denizli' }, { code: 'TR-21', countryCode: 'TR', name: 'Diyarbakir' }, { code: 'TR-81', countryCode: 'TR', name: 'Duzce' }, { code: 'TR-22', countryCode: 'TR', name: 'Edirne' }, { code: 'TR-23', countryCode: 'TR', name: 'Elazig' }, { code: 'TR-24', countryCode: 'TR', name: 'Erzincan' }, { code: 'TR-25', countryCode: 'TR', name: 'Erzurum' }, { code: 'TR-26', countryCode: 'TR', name: 'Eskisehir' }, { code: 'TR-27', countryCode: 'TR', name: 'Gaziantep' }, { code: 'TR-28', countryCode: 'TR', name: 'Giresun' }, { code: 'TR-29', countryCode: 'TR', name: 'Gumushane' }, { code: 'TR-30', countryCode: 'TR', name: 'Hakkari' }, { code: 'TR-31', countryCode: 'TR', name: 'Hatay' }, { code: 'TR-76', countryCode: 'TR', name: 'Igdir' }, { code: 'TR-32', countryCode: 'TR', name: 'Isparta' }, { code: 'TR-34', countryCode: 'TR', name: 'Istanbul' }, { code: 'TR-35', countryCode: 'TR', name: 'Izmir' }, { code: 'TR-46', countryCode: 'TR', name: 'Kahramanmaras' }, { code: 'TR-78', countryCode: 'TR', name: 'Karabuk' }, { code: 'TR-70', countryCode: 'TR', name: 'Karaman' }, { code: 'TR-36', countryCode: 'TR', name: 'Kars' }, { code: 'TR-37', countryCode: 'TR', name: 'Kastamonu' }, { code: 'TR-38', countryCode: 'TR', name: 'Kayseri' }, { code: 'TR-79', countryCode: 'TR', name: 'Kilis' }, { code: 'TR-71', countryCode: 'TR', name: 'Kirikkale' }, { code: 'TR-39', countryCode: 'TR', name: 'Kirklareli' }, { code: 'TR-40', countryCode: 'TR', name: 'Kirsehir' }, { code: 'TR-41', countryCode: 'TR', name: 'Kocaeli' }, { code: 'TR-42', countryCode: 'TR', name: 'Konya' }, { code: 'TR-43', countryCode: 'TR', name: 'Kutahya' }, { code: 'TR-44', countryCode: 'TR', name: 'Malatya' }, { code: 'TR-45', countryCode: 'TR', name: 'Manisa' }, { code: 'TR-47', countryCode: 'TR', name: 'Mardin' }, { code: 'TR-33', countryCode: 'TR', name: 'Mersin' }, { code: 'TR-48', countryCode: 'TR', name: 'Mugla' }, { code: 'TR-49', countryCode: 'TR', name: 'Mus' }, { code: 'TR-50', countryCode: 'TR', name: 'Nevsehir' }, { code: 'TR-51', countryCode: 'TR', name: 'Nigde' }, { code: 'TR-52', countryCode: 'TR', name: 'Ordu' }, { code: 'TR-80', countryCode: 'TR', name: 'Osmaniye' }, { code: 'TR-53', countryCode: 'TR', name: 'Rize' }, { code: 'TR-54', countryCode: 'TR', name: 'Sakarya' }, { code: 'TR-55', countryCode: 'TR', name: 'Samsun' }, { code: 'TR-63', countryCode: 'TR', name: 'Sanliurfa' }, { code: 'TR-56', countryCode: 'TR', name: 'Siirt' }, { code: 'TR-57', countryCode: 'TR', name: 'Sinop' }, { code: 'TR-73', countryCode: 'TR', name: 'Sirnak' }, { code: 'TR-58', countryCode: 'TR', name: 'Sivas' }, { code: 'TR-59', countryCode: 'TR', name: 'Tekirdag' }, { code: 'TR-60', countryCode: 'TR', name: 'Tokat' }, { code: 'TR-61', countryCode: 'TR', name: 'Trabzon' }, { code: 'TR-62', countryCode: 'TR', name: 'Tunceli' }, { code: 'TR-64', countryCode: 'TR', name: 'Usak' }, { code: 'TR-65', countryCode: 'TR', name: 'Van' }, { code: 'TR-77', countryCode: 'TR', name: 'Yalova' }, { code: 'TR-66', countryCode: 'TR', name: 'Yozgat' }, { code: 'TR-67', countryCode: 'TR', name: 'Zonguldak' }, { code: 'TT-ARI', countryCode: 'TT', name: 'Arima' }, { code: 'TT-CHA', countryCode: 'TT', name: 'Chaguanas' }, { code: 'TT-CTT', countryCode: 'TT', name: 'Couva-Tabaquite-Talparo' }, { code: 'TT-DMN', countryCode: 'TT', name: 'Diego Martin' }, { code: 'TT-MRC', countryCode: 'TT', name: 'Mayaro-Rio Claro' }, { code: 'TT-PED', countryCode: 'TT', name: 'Penal-Debe' }, { code: 'TT-PTF', countryCode: 'TT', name: 'Point Fortin' }, { code: 'TT-POS', countryCode: 'TT', name: 'Port of Spain' }, { code: 'TT-PRT', countryCode: 'TT', name: 'Princes Town' }, { code: 'TT-SFO', countryCode: 'TT', name: 'San Fernando' }, { code: 'TT-SJL', countryCode: 'TT', name: 'San Juan-Laventille' }, { code: 'TT-SGE', countryCode: 'TT', name: 'Sangre Grande' }, { code: 'TT-SIP', countryCode: 'TT', name: 'Siparia' }, { code: 'TT-TOB', countryCode: 'TT', name: 'Tobago' }, { code: 'TT-TUP', countryCode: 'TT', name: 'Tunapuna-Piarco' }, { code: 'TV-FUN', countryCode: 'TV', name: 'Funafuti' }, { code: 'TW-CHA', countryCode: 'TW', name: 'Changhua' }, { code: 'TW-CYQ', countryCode: 'TW', name: 'Chiayi' }, { code: 'TW-HSQ', countryCode: 'TW', name: 'Hsinchu' }, { code: 'TW-HUA', countryCode: 'TW', name: 'Hualien' }, { code: 'TW-KHH', countryCode: 'TW', name: 'Kaohsiung' }, { code: 'TW-KEE', countryCode: 'TW', name: 'Keelung' }, { code: 'TW-KIN', countryCode: 'TW', name: 'Kinmen' }, { code: 'TW-LIE', countryCode: 'TW', name: 'Lienchiang' }, { code: 'TW-MIA', countryCode: 'TW', name: 'Miaoli' }, { code: 'TW-NAN', countryCode: 'TW', name: 'Nantou' }, { code: 'TW-NWT', countryCode: 'TW', name: 'New Taipei' }, { code: 'TW-PEN', countryCode: 'TW', name: 'Penghu' }, { code: 'TW-PIF', countryCode: 'TW', name: 'Pingtung' }, { code: 'TW-TXG', countryCode: 'TW', name: 'Taichung' }, { code: 'TW-TNN', countryCode: 'TW', name: 'Tainan' }, { code: 'TW-TPE', countryCode: 'TW', name: 'Taipei' }, { code: 'TW-TTT', countryCode: 'TW', name: 'Taitung' }, { code: 'TW-TAO', countryCode: 'TW', name: 'Taoyuan' }, { code: 'TW-ILA', countryCode: 'TW', name: 'Yilan' }, { code: 'TW-YUN', countryCode: 'TW', name: 'Yunlin' }, { code: 'TZ-01', countryCode: 'TZ', name: 'Arusha' }, { code: 'TZ-02', countryCode: 'TZ', name: 'Dar es Salaam' }, { code: 'TZ-03', countryCode: 'TZ', name: 'Dodoma' }, { code: 'TZ-04', countryCode: 'TZ', name: 'Iringa' }, { code: 'TZ-05', countryCode: 'TZ', name: 'Kagera' }, { code: 'TZ-06', countryCode: 'TZ', name: 'Kaskazini Pemba' }, { code: 'TZ-07', countryCode: 'TZ', name: 'Kaskazini Unguja' }, { code: 'TZ-08', countryCode: 'TZ', name: 'Kigoma' }, { code: 'TZ-09', countryCode: 'TZ', name: 'Kilimanjaro' }, { code: 'TZ-10', countryCode: 'TZ', name: 'Kusini Pemba' }, { code: 'TZ-11', countryCode: 'TZ', name: 'Kusini Unguja' }, { code: 'TZ-12', countryCode: 'TZ', name: 'Lindi' }, { code: 'TZ-26', countryCode: 'TZ', name: 'Manyara' }, { code: 'TZ-13', countryCode: 'TZ', name: 'Mara' }, { code: 'TZ-14', countryCode: 'TZ', name: 'Mbeya' }, { code: 'TZ-15', countryCode: 'TZ', name: 'Mjini Magharibi' }, { code: 'TZ-16', countryCode: 'TZ', name: 'Morogoro' }, { code: 'TZ-17', countryCode: 'TZ', name: 'Mtwara' }, { code: 'TZ-18', countryCode: 'TZ', name: 'Mwanza' }, { code: 'TZ-19', countryCode: 'TZ', name: 'Pwani' }, { code: 'TZ-20', countryCode: 'TZ', name: 'Rukwa' }, { code: 'TZ-21', countryCode: 'TZ', name: 'Ruvuma' }, { code: 'TZ-22', countryCode: 'TZ', name: 'Shinyanga' }, { code: 'TZ-23', countryCode: 'TZ', name: 'Singida' }, { code: 'TZ-24', countryCode: 'TZ', name: 'Tabora' }, { code: 'TZ-25', countryCode: 'TZ', name: 'Tanga' }, { code: 'UA-43', countryCode: 'UA', name: 'Avtonomna Respublika Krym' }, { code: 'UA-71', countryCode: 'UA', name: 'Cherkaska oblast' }, { code: 'UA-74', countryCode: 'UA', name: 'Chernihivska oblast' }, { code: 'UA-77', countryCode: 'UA', name: 'Chernivetska oblast' }, { code: 'UA-12', countryCode: 'UA', name: 'Dnipropetrovska oblast' }, { code: 'UA-14', countryCode: 'UA', name: 'Donetska oblast' }, { code: 'UA-26', countryCode: 'UA', name: 'Ivano-Frankivska oblast' }, { code: 'UA-63', countryCode: 'UA', name: 'Kharkivska oblast' }, { code: 'UA-65', countryCode: 'UA', name: 'Khersonska oblast' }, { code: 'UA-68', countryCode: 'UA', name: 'Khmelnytska oblast' }, { code: 'UA-35', countryCode: 'UA', name: 'Kirovohradska oblast' }, { code: 'UA-30', countryCode: 'UA', name: 'Kyiv' }, { code: 'UA-32', countryCode: 'UA', name: 'Kyivska oblast' }, { code: 'UA-09', countryCode: 'UA', name: 'Luhanska oblast' }, { code: 'UA-46', countryCode: 'UA', name: 'Lvivska oblast' }, { code: 'UA-48', countryCode: 'UA', name: 'Mykolaivska oblast' }, { code: 'UA-51', countryCode: 'UA', name: 'Odeska oblast' }, { code: 'UA-53', countryCode: 'UA', name: 'Poltavska oblast' }, { code: 'UA-56', countryCode: 'UA', name: 'Rivnenska oblast' }, { code: 'UA-40', countryCode: 'UA', name: "Sevastopol'" }, { code: 'UA-59', countryCode: 'UA', name: 'Sumska oblast' }, { code: 'UA-61', countryCode: 'UA', name: 'Ternopilska oblast' }, { code: 'UA-05', countryCode: 'UA', name: 'Vinnytska oblast' }, { code: 'UA-07', countryCode: 'UA', name: 'Volynska oblast' }, { code: 'UA-21', countryCode: 'UA', name: 'Zakarpatska oblast' }, { code: 'UA-23', countryCode: 'UA', name: 'Zaporizka oblast' }, { code: 'UA-18', countryCode: 'UA', name: 'Zhytomyrska oblast' }, { code: 'UG-317', countryCode: 'UG', name: 'Abim' }, { code: 'UG-301', countryCode: 'UG', name: 'Adjumani' }, { code: 'UG-322', countryCode: 'UG', name: 'Agago' }, { code: 'UG-323', countryCode: 'UG', name: 'Alebtong' }, { code: 'UG-314', countryCode: 'UG', name: 'Amolatar' }, { code: 'UG-324', countryCode: 'UG', name: 'Amudat' }, { code: 'UG-216', countryCode: 'UG', name: 'Amuria' }, { code: 'UG-319', countryCode: 'UG', name: 'Amuru' }, { code: 'UG-302', countryCode: 'UG', name: 'Apac' }, { code: 'UG-303', countryCode: 'UG', name: 'Arua' }, { code: 'UG-217', countryCode: 'UG', name: 'Budaka' }, { code: 'UG-223', countryCode: 'UG', name: 'Bududa' }, { code: 'UG-201', countryCode: 'UG', name: 'Bugiri' }, { code: 'UG-117', countryCode: 'UG', name: 'Buikwe' }, { code: 'UG-224', countryCode: 'UG', name: 'Bukedea' }, { code: 'UG-118', countryCode: 'UG', name: 'Bukomansibi' }, { code: 'UG-218', countryCode: 'UG', name: 'Bukwa' }, { code: 'UG-225', countryCode: 'UG', name: 'Bulambuli' }, { code: 'UG-419', countryCode: 'UG', name: 'Buliisa' }, { code: 'UG-401', countryCode: 'UG', name: 'Bundibugyo' }, { code: 'UG-402', countryCode: 'UG', name: 'Bushenyi' }, { code: 'UG-202', countryCode: 'UG', name: 'Busia' }, { code: 'UG-219', countryCode: 'UG', name: 'Butaleja' }, { code: 'UG-120', countryCode: 'UG', name: 'Buvuma' }, { code: 'UG-226', countryCode: 'UG', name: 'Buyende' }, { code: 'UG-318', countryCode: 'UG', name: 'Dokolo' }, { code: 'UG-121', countryCode: 'UG', name: 'Gomba' }, { code: 'UG-304', countryCode: 'UG', name: 'Gulu' }, { code: 'UG-403', countryCode: 'UG', name: 'Hoima' }, { code: 'UG-203', countryCode: 'UG', name: 'Iganga' }, { code: 'UG-417', countryCode: 'UG', name: 'Isingiro' }, { code: 'UG-204', countryCode: 'UG', name: 'Jinja' }, { code: 'UG-315', countryCode: 'UG', name: 'Kaabong' }, { code: 'UG-404', countryCode: 'UG', name: 'Kabale' }, { code: 'UG-405', countryCode: 'UG', name: 'Kabarole' }, { code: 'UG-213', countryCode: 'UG', name: 'Kaberamaido' }, { code: 'UG-101', countryCode: 'UG', name: 'Kalangala' }, { code: 'UG-220', countryCode: 'UG', name: 'Kaliro' }, { code: 'UG-102', countryCode: 'UG', name: 'Kampala' }, { code: 'UG-205', countryCode: 'UG', name: 'Kamuli' }, { code: 'UG-413', countryCode: 'UG', name: 'Kamwenge' }, { code: 'UG-414', countryCode: 'UG', name: 'Kanungu' }, { code: 'UG-206', countryCode: 'UG', name: 'Kapchorwa' }, { code: 'UG-406', countryCode: 'UG', name: 'Kasese' }, { code: 'UG-207', countryCode: 'UG', name: 'Katakwi' }, { code: 'UG-112', countryCode: 'UG', name: 'Kayunga' }, { code: 'UG-407', countryCode: 'UG', name: 'Kibaale' }, { code: 'UG-103', countryCode: 'UG', name: 'Kiboga' }, { code: 'UG-227', countryCode: 'UG', name: 'Kibuku' }, { code: 'UG-420', countryCode: 'UG', name: 'Kiryandongo' }, { code: 'UG-408', countryCode: 'UG', name: 'Kisoro' }, { code: 'UG-305', countryCode: 'UG', name: 'Kitgum' }, { code: 'UG-316', countryCode: 'UG', name: 'Koboko' }, { code: 'UG-326', countryCode: 'UG', name: 'Kole' }, { code: 'UG-306', countryCode: 'UG', name: 'Kotido' }, { code: 'UG-208', countryCode: 'UG', name: 'Kumi' }, { code: 'UG-228', countryCode: 'UG', name: 'Kween' }, { code: 'UG-123', countryCode: 'UG', name: 'Kyankwanzi' }, { code: 'UG-421', countryCode: 'UG', name: 'Kyegegwa' }, { code: 'UG-415', countryCode: 'UG', name: 'Kyenjojo' }, { code: 'UG-307', countryCode: 'UG', name: 'Lira' }, { code: 'UG-229', countryCode: 'UG', name: 'Luuka' }, { code: 'UG-104', countryCode: 'UG', name: 'Luwero' }, { code: 'UG-124', countryCode: 'UG', name: 'Lwengo' }, { code: 'UG-116', countryCode: 'UG', name: 'Lyantonde' }, { code: 'UG-221', countryCode: 'UG', name: 'Manafwa' }, { code: 'UG-320', countryCode: 'UG', name: 'Maracha' }, { code: 'UG-105', countryCode: 'UG', name: 'Masaka' }, { code: 'UG-409', countryCode: 'UG', name: 'Masindi' }, { code: 'UG-214', countryCode: 'UG', name: 'Mayuge' }, { code: 'UG-209', countryCode: 'UG', name: 'Mbale' }, { code: 'UG-410', countryCode: 'UG', name: 'Mbarara' }, { code: 'UG-422', countryCode: 'UG', name: 'Mitooma' }, { code: 'UG-114', countryCode: 'UG', name: 'Mityana' }, { code: 'UG-308', countryCode: 'UG', name: 'Moroto' }, { code: 'UG-309', countryCode: 'UG', name: 'Moyo' }, { code: 'UG-106', countryCode: 'UG', name: 'Mpigi' }, { code: 'UG-107', countryCode: 'UG', name: 'Mubende' }, { code: 'UG-108', countryCode: 'UG', name: 'Mukono' }, { code: 'UG-311', countryCode: 'UG', name: 'Nakapiripirit' }, { code: 'UG-115', countryCode: 'UG', name: 'Nakaseke' }, { code: 'UG-109', countryCode: 'UG', name: 'Nakasongola' }, { code: 'UG-230', countryCode: 'UG', name: 'Namayingo' }, { code: 'UG-222', countryCode: 'UG', name: 'Namutumba' }, { code: 'UG-328', countryCode: 'UG', name: 'Napak' }, { code: 'UG-310', countryCode: 'UG', name: 'Nebbi' }, { code: 'UG-231', countryCode: 'UG', name: 'Ngora' }, { code: 'UG-423', countryCode: 'UG', name: 'Ntoroko' }, { code: 'UG-411', countryCode: 'UG', name: 'Ntungamo' }, { code: 'UG-330', countryCode: 'UG', name: 'Otuke' }, { code: 'UG-321', countryCode: 'UG', name: 'Oyam' }, { code: 'UG-312', countryCode: 'UG', name: 'Pader' }, { code: 'UG-210', countryCode: 'UG', name: 'Pallisa' }, { code: 'UG-110', countryCode: 'UG', name: 'Rakai' }, { code: 'UG-424', countryCode: 'UG', name: 'Rubirizi' }, { code: 'UG-412', countryCode: 'UG', name: 'Rukungiri' }, { code: 'UG-111', countryCode: 'UG', name: 'Sembabule' }, { code: 'UG-232', countryCode: 'UG', name: 'Serere' }, { code: 'UG-425', countryCode: 'UG', name: 'Sheema' }, { code: 'UG-215', countryCode: 'UG', name: 'Sironko' }, { code: 'UG-211', countryCode: 'UG', name: 'Soroti' }, { code: 'UG-212', countryCode: 'UG', name: 'Tororo' }, { code: 'UG-113', countryCode: 'UG', name: 'Wakiso' }, { code: 'UG-313', countryCode: 'UG', name: 'Yumbe' }, { code: 'UG-331', countryCode: 'UG', name: 'Zombo' }, { code: 'UM-95', countryCode: 'UM', name: 'Palmyra Atoll' }, { code: 'US-AL', countryCode: 'US', name: 'Alabama' }, { code: 'US-AK', countryCode: 'US', name: 'Alaska' }, { code: 'US-AZ', countryCode: 'US', name: 'Arizona' }, { code: 'US-AR', countryCode: 'US', name: 'Arkansas' }, { code: 'US-CA', countryCode: 'US', name: 'California' }, { code: 'US-CO', countryCode: 'US', name: 'Colorado' }, { code: 'US-CT', countryCode: 'US', name: 'Connecticut' }, { code: 'US-DE', countryCode: 'US', name: 'Delaware' }, { code: 'US-DC', countryCode: 'US', name: 'District of Columbia' }, { code: 'US-FL', countryCode: 'US', name: 'Florida' }, { code: 'US-GA', countryCode: 'US', name: 'Georgia' }, { code: 'US-HI', countryCode: 'US', name: 'Hawaii' }, { code: 'US-ID', countryCode: 'US', name: 'Idaho' }, { code: 'US-IL', countryCode: 'US', name: 'Illinois' }, { code: 'US-IN', countryCode: 'US', name: 'Indiana' }, { code: 'US-IA', countryCode: 'US', name: 'Iowa' }, { code: 'US-KS', countryCode: 'US', name: 'Kansas' }, { code: 'US-KY', countryCode: 'US', name: 'Kentucky' }, { code: 'US-LA', countryCode: 'US', name: 'Louisiana' }, { code: 'US-ME', countryCode: 'US', name: 'Maine' }, { code: 'US-MD', countryCode: 'US', name: 'Maryland' }, { code: 'US-MA', countryCode: 'US', name: 'Massachusetts' }, { code: 'US-MI', countryCode: 'US', name: 'Michigan' }, { code: 'US-MN', countryCode: 'US', name: 'Minnesota' }, { code: 'US-MS', countryCode: 'US', name: 'Mississippi' }, { code: 'US-MO', countryCode: 'US', name: 'Missouri' }, { code: 'US-MT', countryCode: 'US', name: 'Montana' }, { code: 'US-NE', countryCode: 'US', name: 'Nebraska' }, { code: 'US-NV', countryCode: 'US', name: 'Nevada' }, { code: 'US-NH', countryCode: 'US', name: 'New Hampshire' }, { code: 'US-NJ', countryCode: 'US', name: 'New Jersey' }, { code: 'US-NM', countryCode: 'US', name: 'New Mexico' }, { code: 'US-NY', countryCode: 'US', name: 'New York' }, { code: 'US-NC', countryCode: 'US', name: 'North Carolina' }, { code: 'US-ND', countryCode: 'US', name: 'North Dakota' }, { code: 'US-OH', countryCode: 'US', name: 'Ohio' }, { code: 'US-OK', countryCode: 'US', name: 'Oklahoma' }, { code: 'US-OR', countryCode: 'US', name: 'Oregon' }, { code: 'US-PA', countryCode: 'US', name: 'Pennsylvania' }, { code: 'US-RI', countryCode: 'US', name: 'Rhode Island' }, { code: 'US-SC', countryCode: 'US', name: 'South Carolina' }, { code: 'US-SD', countryCode: 'US', name: 'South Dakota' }, { code: 'US-TN', countryCode: 'US', name: 'Tennessee' }, { code: 'US-TX', countryCode: 'US', name: 'Texas' }, { code: 'US-UT', countryCode: 'US', name: 'Utah' }, { code: 'US-VT', countryCode: 'US', name: 'Vermont' }, { code: 'US-VA', countryCode: 'US', name: 'Virginia' }, { code: 'US-WA', countryCode: 'US', name: 'Washington' }, { code: 'US-WV', countryCode: 'US', name: 'West Virginia' }, { code: 'US-WI', countryCode: 'US', name: 'Wisconsin' }, { code: 'US-WY', countryCode: 'US', name: 'Wyoming' }, { code: 'UY-AR', countryCode: 'UY', name: 'Artigas' }, { code: 'UY-CA', countryCode: 'UY', name: 'Canelones' }, { code: 'UY-CL', countryCode: 'UY', name: 'Cerro Largo' }, { code: 'UY-CO', countryCode: 'UY', name: 'Colonia' }, { code: 'UY-DU', countryCode: 'UY', name: 'Durazno' }, { code: 'UY-FS', countryCode: 'UY', name: 'Flores' }, { code: 'UY-FD', countryCode: 'UY', name: 'Florida' }, { code: 'UY-LA', countryCode: 'UY', name: 'Lavalleja' }, { code: 'UY-MA', countryCode: 'UY', name: 'Maldonado' }, { code: 'UY-MO', countryCode: 'UY', name: 'Montevideo' }, { code: 'UY-PA', countryCode: 'UY', name: 'Paysandu' }, { code: 'UY-RN', countryCode: 'UY', name: 'Rio Negro' }, { code: 'UY-RV', countryCode: 'UY', name: 'Rivera' }, { code: 'UY-RO', countryCode: 'UY', name: 'Rocha' }, { code: 'UY-SA', countryCode: 'UY', name: 'Salto' }, { code: 'UY-SJ', countryCode: 'UY', name: 'San Jose' }, { code: 'UY-SO', countryCode: 'UY', name: 'Soriano' }, { code: 'UY-TA', countryCode: 'UY', name: 'Tacuarembo' }, { code: 'UY-TT', countryCode: 'UY', name: 'Treinta y Tres' }, { code: 'UZ-AN', countryCode: 'UZ', name: 'Andijon' }, { code: 'UZ-BU', countryCode: 'UZ', name: 'Buxoro' }, { code: 'UZ-FA', countryCode: 'UZ', name: "Farg'ona" }, { code: 'UZ-JI', countryCode: 'UZ', name: 'Jizzax' }, { code: 'UZ-NG', countryCode: 'UZ', name: 'Namangan' }, { code: 'UZ-NW', countryCode: 'UZ', name: 'Navoiy' }, { code: 'UZ-QA', countryCode: 'UZ', name: 'Qashqadaryo' }, { code: 'UZ-QR', countryCode: 'UZ', name: "Qoraqalpog'iston Respublikasi" }, { code: 'UZ-SA', countryCode: 'UZ', name: 'Samarqand' }, { code: 'UZ-SI', countryCode: 'UZ', name: 'Sirdaryo' }, { code: 'UZ-SU', countryCode: 'UZ', name: 'Surxondaryo' }, { code: 'UZ-TK', countryCode: 'UZ', name: 'Toshkent' }, { code: 'UZ-XO', countryCode: 'UZ', name: 'Xorazm' }, { code: 'VC-01', countryCode: 'VC', name: 'Charlotte' }, { code: 'VC-04', countryCode: 'VC', name: 'Saint George' }, { code: 'VE-Z', countryCode: 'VE', name: 'Amazonas' }, { code: 'VE-B', countryCode: 'VE', name: 'Anzoategui' }, { code: 'VE-C', countryCode: 'VE', name: 'Apure' }, { code: 'VE-D', countryCode: 'VE', name: 'Aragua' }, { code: 'VE-E', countryCode: 'VE', name: 'Barinas' }, { code: 'VE-F', countryCode: 'VE', name: 'Bolivar' }, { code: 'VE-G', countryCode: 'VE', name: 'Carabobo' }, { code: 'VE-H', countryCode: 'VE', name: 'Cojedes' }, { code: 'VE-Y', countryCode: 'VE', name: 'Delta Amacuro' }, { code: 'VE-A', countryCode: 'VE', name: 'Distrito Capital' }, { code: 'VE-I', countryCode: 'VE', name: 'Falcon' }, { code: 'VE-J', countryCode: 'VE', name: 'Guarico' }, { code: 'VE-K', countryCode: 'VE', name: 'Lara' }, { code: 'VE-L', countryCode: 'VE', name: 'Merida' }, { code: 'VE-M', countryCode: 'VE', name: 'Miranda' }, { code: 'VE-N', countryCode: 'VE', name: 'Monagas' }, { code: 'VE-O', countryCode: 'VE', name: 'Nueva Esparta' }, { code: 'VE-P', countryCode: 'VE', name: 'Portuguesa' }, { code: 'VE-R', countryCode: 'VE', name: 'Sucre' }, { code: 'VE-S', countryCode: 'VE', name: 'Tachira' }, { code: 'VE-T', countryCode: 'VE', name: 'Trujillo' }, { code: 'VE-X', countryCode: 'VE', name: 'Vargas' }, { code: 'VE-U', countryCode: 'VE', name: 'Yaracuy' }, { code: 'VE-V', countryCode: 'VE', name: 'Zulia' }, { code: 'VN-44', countryCode: 'VN', name: 'An Giang' }, { code: 'VN-54', countryCode: 'VN', name: 'Bac Giang' }, { code: 'VN-53', countryCode: 'VN', name: 'Bac Kan' }, { code: 'VN-55', countryCode: 'VN', name: 'Bac Lieu' }, { code: 'VN-56', countryCode: 'VN', name: 'Bac Ninh' }, { code: 'VN-50', countryCode: 'VN', name: 'Ben Tre' }, { code: 'VN-31', countryCode: 'VN', name: 'Binh Dinh' }, { code: 'VN-57', countryCode: 'VN', name: 'Binh Duong' }, { code: 'VN-58', countryCode: 'VN', name: 'Binh Phuoc' }, { code: 'VN-40', countryCode: 'VN', name: 'Binh Thuan' }, { code: 'VN-59', countryCode: 'VN', name: 'Ca Mau' }, { code: 'VN-CT', countryCode: 'VN', name: 'Can Tho' }, { code: 'VN-04', countryCode: 'VN', name: 'Cao Bang' }, { code: 'VN-DN', countryCode: 'VN', name: 'Da Nang' }, { code: 'VN-33', countryCode: 'VN', name: 'Dak Lak' }, { code: 'VN-71', countryCode: 'VN', name: 'Dien Bien' }, { code: 'VN-39', countryCode: 'VN', name: 'Dong Nai' }, { code: 'VN-45', countryCode: 'VN', name: 'Dong Thap' }, { code: 'VN-30', countryCode: 'VN', name: 'Gia Lai' }, { code: 'VN-03', countryCode: 'VN', name: 'Ha Giang' }, { code: 'VN-63', countryCode: 'VN', name: 'Ha Nam' }, { code: 'VN-HN', countryCode: 'VN', name: 'Ha Noi' }, { code: 'VN-23', countryCode: 'VN', name: 'Ha Tinh' }, { code: 'VN-61', countryCode: 'VN', name: 'Hai Duong' }, { code: 'VN-HP', countryCode: 'VN', name: 'Hai Phong' }, { code: 'VN-SG', countryCode: 'VN', name: 'Ho Chi Minh' }, { code: 'VN-14', countryCode: 'VN', name: 'Hoa Binh' }, { code: 'VN-66', countryCode: 'VN', name: 'Hung Yen' }, { code: 'VN-34', countryCode: 'VN', name: 'Khanh Hoa' }, { code: 'VN-47', countryCode: 'VN', name: 'Kien Giang' }, { code: 'VN-01', countryCode: 'VN', name: 'Lai Chau' }, { code: 'VN-35', countryCode: 'VN', name: 'Lam Dong' }, { code: 'VN-09', countryCode: 'VN', name: 'Lang Son' }, { code: 'VN-02', countryCode: 'VN', name: 'Lao Cai' }, { code: 'VN-41', countryCode: 'VN', name: 'Long An' }, { code: 'VN-67', countryCode: 'VN', name: 'Nam Dinh' }, { code: 'VN-22', countryCode: 'VN', name: 'Nghe An' }, { code: 'VN-18', countryCode: 'VN', name: 'Ninh Binh' }, { code: 'VN-36', countryCode: 'VN', name: 'Ninh Thuan' }, { code: 'VN-68', countryCode: 'VN', name: 'Phu Tho' }, { code: 'VN-32', countryCode: 'VN', name: 'Phu Yen' }, { code: 'VN-24', countryCode: 'VN', name: 'Quang Binh' }, { code: 'VN-27', countryCode: 'VN', name: 'Quang Nam' }, { code: 'VN-29', countryCode: 'VN', name: 'Quang Ngai' }, { code: 'VN-13', countryCode: 'VN', name: 'Quang Ninh' }, { code: 'VN-25', countryCode: 'VN', name: 'Quang Tri' }, { code: 'VN-52', countryCode: 'VN', name: 'Soc Trang' }, { code: 'VN-05', countryCode: 'VN', name: 'Son La' }, { code: 'VN-37', countryCode: 'VN', name: 'Tay Ninh' }, { code: 'VN-20', countryCode: 'VN', name: 'Thai Binh' }, { code: 'VN-69', countryCode: 'VN', name: 'Thai Nguyen' }, { code: 'VN-21', countryCode: 'VN', name: 'Thanh Hoa' }, { code: 'VN-26', countryCode: 'VN', name: 'Thua Thien-Hue' }, { code: 'VN-46', countryCode: 'VN', name: 'Tien Giang' }, { code: 'VN-51', countryCode: 'VN', name: 'Tra Vinh' }, { code: 'VN-07', countryCode: 'VN', name: 'Tuyen Quang' }, { code: 'VN-49', countryCode: 'VN', name: 'Vinh Long' }, { code: 'VN-70', countryCode: 'VN', name: 'Vinh Phuc' }, { code: 'VN-06', countryCode: 'VN', name: 'Yen Bai' }, { code: 'VU-MAP', countryCode: 'VU', name: 'Malampa' }, { code: 'VU-SAM', countryCode: 'VU', name: 'Sanma' }, { code: 'VU-SEE', countryCode: 'VU', name: 'Shefa' }, { code: 'VU-TAE', countryCode: 'VU', name: 'Tafea' }, { code: 'VU-TOB', countryCode: 'VU', name: 'Torba' }, { code: 'WF-UV', countryCode: 'WF', name: 'Uvea' }, { code: 'WS-AA', countryCode: 'WS', name: "A'ana" }, { code: 'WS-AT', countryCode: 'WS', name: 'Atua' }, { code: 'WS-GI', countryCode: 'WS', name: 'Gagaifomauga' }, { code: 'WS-PA', countryCode: 'WS', name: 'Palauli' }, { code: 'WS-TU', countryCode: 'WS', name: 'Tuamasaga' }, { code: 'YE-AD', countryCode: 'YE', name: "'Adan" }, { code: 'YE-AM', countryCode: 'YE', name: "'Amran" }, { code: 'YE-AB', countryCode: 'YE', name: 'Abyan' }, { code: 'YE-DA', countryCode: 'YE', name: "Ad Dali'" }, { code: 'YE-BA', countryCode: 'YE', name: "Al Bayda'" }, { code: 'YE-HU', countryCode: 'YE', name: 'Al Hudaydah' }, { code: 'YE-JA', countryCode: 'YE', name: 'Al Jawf' }, { code: 'YE-MR', countryCode: 'YE', name: 'Al Mahrah' }, { code: 'YE-MW', countryCode: 'YE', name: 'Al Mahwit' }, { code: 'YE-SA', countryCode: 'YE', name: "Amanat al 'Asimah" }, { code: 'YE-DH', countryCode: 'YE', name: 'Dhamar' }, { code: 'YE-HD', countryCode: 'YE', name: 'Hadramawt' }, { code: 'YE-HJ', countryCode: 'YE', name: 'Hajjah' }, { code: 'YE-IB', countryCode: 'YE', name: 'Ibb' }, { code: 'YE-LA', countryCode: 'YE', name: 'Lahij' }, { code: 'YE-MA', countryCode: 'YE', name: "Ma'rib" }, { code: 'YE-RA', countryCode: 'YE', name: 'Raymah' }, { code: 'YE-SD', countryCode: 'YE', name: "Sa'dah" }, { code: 'YE-SN', countryCode: 'YE', name: "San'a'" }, { code: 'YE-SH', countryCode: 'YE', name: 'Shabwah' }, { code: 'YE-TA', countryCode: 'YE', name: "Ta'izz" }, { code: 'ZA-EC', countryCode: 'ZA', name: 'Eastern Cape' }, { code: 'ZA-FS', countryCode: 'ZA', name: 'Free State' }, { code: 'ZA-GT', countryCode: 'ZA', name: 'Gauteng' }, { code: 'ZA-NL', countryCode: 'ZA', name: 'Kwazulu-Natal' }, { code: 'ZA-LP', countryCode: 'ZA', name: 'Limpopo' }, { code: 'ZA-MP', countryCode: 'ZA', name: 'Mpumalanga' }, { code: 'ZA-NW', countryCode: 'ZA', name: 'North-West' }, { code: 'ZA-NC', countryCode: 'ZA', name: 'Northern Cape' }, { code: 'ZA-WC', countryCode: 'ZA', name: 'Western Cape' }, { code: 'ZM-02', countryCode: 'ZM', name: 'Central' }, { code: 'ZM-08', countryCode: 'ZM', name: 'Copperbelt' }, { code: 'ZM-03', countryCode: 'ZM', name: 'Eastern' }, { code: 'ZM-04', countryCode: 'ZM', name: 'Luapula' }, { code: 'ZM-09', countryCode: 'ZM', name: 'Lusaka' }, { code: 'ZM-06', countryCode: 'ZM', name: 'North-Western' }, { code: 'ZM-05', countryCode: 'ZM', name: 'Northern' }, { code: 'ZM-07', countryCode: 'ZM', name: 'Southern' }, { code: 'ZM-01', countryCode: 'ZM', name: 'Western' }, { code: 'ZW-BU', countryCode: 'ZW', name: 'Bulawayo' }, { code: 'ZW-HA', countryCode: 'ZW', name: 'Harare' }, { code: 'ZW-MA', countryCode: 'ZW', name: 'Manicaland' }, { code: 'ZW-MC', countryCode: 'ZW', name: 'Mashonaland Central' }, { code: 'ZW-ME', countryCode: 'ZW', name: 'Mashonaland East' }, { code: 'ZW-MW', countryCode: 'ZW', name: 'Mashonaland West' }, { code: 'ZW-MV', countryCode: 'ZW', name: 'Masvingo' }, { code: 'ZW-MN', countryCode: 'ZW', name: 'Matabeleland North' }, { code: 'ZW-MS', countryCode: 'ZW', name: 'Matabeleland South' }, { code: 'ZW-MI', countryCode: 'ZW', name: 'Midlands' } ]; ================================================ FILE: packages/evershop/src/lib/locale/timezones.ts ================================================ export interface Timezone { code: string; name: string; } export const timezones: Timezone[] = [ { code: 'Australia/Darwin', name: 'AUS Central Standard Time (Australia/Darwin)' }, { code: 'Australia/Melbourne', name: 'AUS Eastern Standard Time (Australia/Melbourne)' }, { code: 'Australia/Sydney', name: 'AUS Eastern Standard Time (Australia/Sydney)' }, { code: 'Asia/Kabul', name: 'Afghanistan Standard Time (Asia/Kabul)' }, { code: 'America/Anchorage', name: 'Alaskan Standard Time (America/Anchorage)' }, { code: 'America/Juneau', name: 'Alaskan Standard Time (America/Juneau)' }, { code: 'America/Nome', name: 'Alaskan Standard Time (America/Nome)' }, { code: 'America/Sitka', name: 'Alaskan Standard Time (America/Sitka)' }, { code: 'America/Yakutat', name: 'Alaskan Standard Time (America/Yakutat)' }, { code: 'Asia/Aden', name: 'Arab Standard Time (Asia/Aden)' }, { code: 'Asia/Bahrain', name: 'Arab Standard Time (Asia/Bahrain)' }, { code: 'Asia/Kuwait', name: 'Arab Standard Time (Asia/Kuwait)' }, { code: 'Asia/Qatar', name: 'Arab Standard Time (Asia/Qatar)' }, { code: 'Asia/Riyadh', name: 'Arab Standard Time (Asia/Riyadh)' }, { code: 'Asia/Dubai', name: 'Arabian Standard Time (Asia/Dubai)' }, { code: 'Asia/Muscat', name: 'Arabian Standard Time (Asia/Muscat)' }, { code: 'Etc/GMT-4', name: 'Arabian Standard Time (Etc/GMT-4)' }, { code: 'Asia/Baghdad', name: 'Arabic Standard Time (Asia/Baghdad)' }, { code: 'America/Argentina/La_Rioja', name: 'Argentina Standard Time (America/Argentina/La_Rioja)' }, { code: 'America/Argentina/Rio_Gallegos', name: 'Argentina Standard Time (America/Argentina/Rio_Gallegos)' }, { code: 'America/Argentina/Salta', name: 'Argentina Standard Time (America/Argentina/Salta)' }, { code: 'America/Argentina/San_Juan', name: 'Argentina Standard Time (America/Argentina/San_Juan)' }, { code: 'America/Argentina/San_Luis', name: 'Argentina Standard Time (America/Argentina/San_Luis)' }, { code: 'America/Argentina/Tucuman', name: 'Argentina Standard Time (America/Argentina/Tucuman)' }, { code: 'America/Argentina/Ushuaia', name: 'Argentina Standard Time (America/Argentina/Ushuaia)' }, { code: 'America/Buenos_Aires', name: 'Argentina Standard Time (America/Buenos_Aires)' }, { code: 'America/Catamarca', name: 'Argentina Standard Time (America/Catamarca)' }, { code: 'America/Cordoba', name: 'Argentina Standard Time (America/Cordoba)' }, { code: 'America/Jujuy', name: 'Argentina Standard Time (America/Jujuy)' }, { code: 'America/Mendoza', name: 'Argentina Standard Time (America/Mendoza)' }, { code: 'America/Glace_Bay', name: 'Atlantic Standard Time (America/Glace_Bay)' }, { code: 'America/Goose_Bay', name: 'Atlantic Standard Time (America/Goose_Bay)' }, { code: 'America/Halifax', name: 'Atlantic Standard Time (America/Halifax)' }, { code: 'America/Moncton', name: 'Atlantic Standard Time (America/Moncton)' }, { code: 'America/Thule', name: 'Atlantic Standard Time (America/Thule)' }, { code: 'Atlantic/Bermuda', name: 'Atlantic Standard Time (Atlantic/Bermuda)' }, { code: 'Asia/Baku', name: 'Azerbaijan Standard Time (Asia/Baku)' }, { code: 'America/Scoresbysund', name: 'Azores Standard Time (America/Scoresbysund)' }, { code: 'Atlantic/Azores', name: 'Azores Standard Time (Atlantic/Azores)' }, { code: 'America/Bahia', name: 'Bahia Standard Time (America/Bahia)' }, { code: 'Asia/Dhaka', name: 'Bangladesh Standard Time (Asia/Dhaka)' }, { code: 'Asia/Thimphu', name: 'Bangladesh Standard Time (Asia/Thimphu)' }, { code: 'America/Regina', name: 'Canada Central Standard Time (America/Regina)' }, { code: 'America/Swift_Current', name: 'Canada Central Standard Time (America/Swift_Current)' }, { code: 'Atlantic/Cape_Verde', name: 'Cape Verde Standard Time (Atlantic/Cape_Verde)' }, { code: 'Etc/GMT+1', name: 'Cape Verde Standard Time (Etc/GMT+1)' }, { code: 'Asia/Yerevan', name: 'Caucasus Standard Time (Asia/Yerevan)' }, { code: 'Australia/Adelaide', name: 'Cen. Australia Standard Time (Australia/Adelaide)' }, { code: 'Australia/Broken_Hill', name: 'Cen. Australia Standard Time (Australia/Broken_Hill)' }, { code: 'America/Belize', name: 'Central America Standard Time (America/Belize)' }, { code: 'America/Costa_Rica', name: 'Central America Standard Time (America/Costa_Rica)' }, { code: 'America/El_Salvador', name: 'Central America Standard Time (America/El_Salvador)' }, { code: 'America/Guatemala', name: 'Central America Standard Time (America/Guatemala)' }, { code: 'America/Managua', name: 'Central America Standard Time (America/Managua)' }, { code: 'America/Tegucigalpa', name: 'Central America Standard Time (America/Tegucigalpa)' }, { code: 'Etc/GMT+6', name: 'Central America Standard Time (Etc/GMT+6)' }, { code: 'Pacific/Galapagos', name: 'Central America Standard Time (Pacific/Galapagos)' }, { code: 'Antarctica/Vostok', name: 'Central Asia Standard Time (Antarctica/Vostok)' }, { code: 'Asia/Almaty', name: 'Central Asia Standard Time (Asia/Almaty)' }, { code: 'Asia/Bishkek', name: 'Central Asia Standard Time (Asia/Bishkek)' }, { code: 'Asia/Qyzylorda', name: 'Central Asia Standard Time (Asia/Qyzylorda)' }, { code: 'Etc/GMT-6', name: 'Central Asia Standard Time (Etc/GMT-6)' }, { code: 'Indian/Chagos', name: 'Central Asia Standard Time (Indian/Chagos)' }, { code: 'America/Campo_Grande', name: 'Central Brazilian Standard Time (America/Campo_Grande)' }, { code: 'America/Cuiaba', name: 'Central Brazilian Standard Time (America/Cuiaba)' }, { code: 'Europe/Belgrade', name: 'Central Europe Standard Time (Europe/Belgrade)' }, { code: 'Europe/Bratislava', name: 'Central Europe Standard Time (Europe/Bratislava)' }, { code: 'Europe/Budapest', name: 'Central Europe Standard Time (Europe/Budapest)' }, { code: 'Europe/Ljubljana', name: 'Central Europe Standard Time (Europe/Ljubljana)' }, { code: 'Europe/Podgorica', name: 'Central Europe Standard Time (Europe/Podgorica)' }, { code: 'Europe/Prague', name: 'Central Europe Standard Time (Europe/Prague)' }, { code: 'Europe/Tirane', name: 'Central Europe Standard Time (Europe/Tirane)' }, { code: 'Europe/Sarajevo', name: 'Central European Standard Time (Europe/Sarajevo)' }, { code: 'Europe/Skopje', name: 'Central European Standard Time (Europe/Skopje)' }, { code: 'Europe/Warsaw', name: 'Central European Standard Time (Europe/Warsaw)' }, { code: 'Europe/Zagreb', name: 'Central European Standard Time (Europe/Zagreb)' }, { code: 'Antarctica/Macquarie', name: 'Central Pacific Standard Time (Antarctica/Macquarie)' }, { code: 'Etc/GMT-11', name: 'Central Pacific Standard Time (Etc/GMT-11)' }, { code: 'Pacific/Efate', name: 'Central Pacific Standard Time (Pacific/Efate)' }, { code: 'Pacific/Guadalcanal', name: 'Central Pacific Standard Time (Pacific/Guadalcanal)' }, { code: 'Pacific/Kosrae', name: 'Central Pacific Standard Time (Pacific/Kosrae)' }, { code: 'Pacific/Noumea', name: 'Central Pacific Standard Time (Pacific/Noumea)' }, { code: 'Pacific/Ponape', name: 'Central Pacific Standard Time (Pacific/Ponape)' }, { code: 'America/Chicago', name: 'Central Standard Time (America/Chicago)' }, { code: 'America/Indiana/Knox', name: 'Central Standard Time (America/Indiana/Knox)' }, { code: 'America/Indiana/Tell_City', name: 'Central Standard Time (America/Indiana/Tell_City)' }, { code: 'America/Matamoros', name: 'Central Standard Time (America/Matamoros)' }, { code: 'America/Menominee', name: 'Central Standard Time (America/Menominee)' }, { code: 'America/North_Dakota/Beulah', name: 'Central Standard Time (America/North_Dakota/Beulah)' }, { code: 'America/North_Dakota/Center', name: 'Central Standard Time (America/North_Dakota/Center)' }, { code: 'America/North_Dakota/New_Salem', name: 'Central Standard Time (America/North_Dakota/New_Salem)' }, { code: 'America/Rainy_River', name: 'Central Standard Time (America/Rainy_River)' }, { code: 'America/Rankin_Inlet', name: 'Central Standard Time (America/Rankin_Inlet)' }, { code: 'America/Resolute', name: 'Central Standard Time (America/Resolute)' }, { code: 'America/Winnipeg', name: 'Central Standard Time (America/Winnipeg)' }, { code: 'CST6CDT', name: 'Central Standard Time (CST6CDT)' }, { code: 'America/Bahia_Banderas', name: 'Central Standard Time (Mexico) (America/Bahia_Banderas)' }, { code: 'America/Cancun', name: 'Central Standard Time (Mexico) (America/Cancun)' }, { code: 'America/Merida', name: 'Central Standard Time (Mexico) (America/Merida)' }, { code: 'America/Mexico_City', name: 'Central Standard Time (Mexico) (America/Mexico_City)' }, { code: 'America/Monterrey', name: 'Central Standard Time (Mexico) (America/Monterrey)' }, { code: 'Asia/Chongqing', name: 'China Standard Time (Asia/Chongqing)' }, { code: 'Asia/Harbin', name: 'China Standard Time (Asia/Harbin)' }, { code: 'Asia/Hong_Kong', name: 'China Standard Time (Asia/Hong_Kong)' }, { code: 'Asia/Kashgar', name: 'China Standard Time (Asia/Kashgar)' }, { code: 'Asia/Macau', name: 'China Standard Time (Asia/Macau)' }, { code: 'Asia/Shanghai', name: 'China Standard Time (Asia/Shanghai)' }, { code: 'Asia/Urumqi', name: 'China Standard Time (Asia/Urumqi)' }, { code: 'Etc/GMT+12', name: 'Dateline Standard Time (Etc/GMT+12)' }, { code: 'Africa/Addis_Ababa', name: 'E. Africa Standard Time (Africa/Addis_Ababa)' }, { code: 'Africa/Asmera', name: 'E. Africa Standard Time (Africa/Asmera)' }, { code: 'Africa/Dar_es_Salaam', name: 'E. Africa Standard Time (Africa/Dar_es_Salaam)' }, { code: 'Africa/Djibouti', name: 'E. Africa Standard Time (Africa/Djibouti)' }, { code: 'Africa/Juba', name: 'E. Africa Standard Time (Africa/Juba)' }, { code: 'Africa/Kampala', name: 'E. Africa Standard Time (Africa/Kampala)' }, { code: 'Africa/Khartoum', name: 'E. Africa Standard Time (Africa/Khartoum)' }, { code: 'Africa/Mogadishu', name: 'E. Africa Standard Time (Africa/Mogadishu)' }, { code: 'Africa/Nairobi', name: 'E. Africa Standard Time (Africa/Nairobi)' }, { code: 'Antarctica/Syowa', name: 'E. Africa Standard Time (Antarctica/Syowa)' }, { code: 'Etc/GMT-3', name: 'E. Africa Standard Time (Etc/GMT-3)' }, { code: 'Indian/Antananarivo', name: 'E. Africa Standard Time (Indian/Antananarivo)' }, { code: 'Indian/Comoro', name: 'E. Africa Standard Time (Indian/Comoro)' }, { code: 'Indian/Mayotte', name: 'E. Africa Standard Time (Indian/Mayotte)' }, { code: 'Australia/Brisbane', name: 'E. Australia Standard Time (Australia/Brisbane)' }, { code: 'Australia/Lindeman', name: 'E. Australia Standard Time (Australia/Lindeman)' }, { code: 'America/Sao_Paulo', name: 'E. South America Standard Time (America/Sao_Paulo)' }, { code: 'America/Detroit', name: 'Eastern Standard Time (America/Detroit)' }, { code: 'America/Grand_Turk', name: 'Eastern Standard Time (America/Grand_Turk)' }, { code: 'America/Havana', name: 'Eastern Standard Time (America/Havana)' }, { code: 'America/Indiana/Petersburg', name: 'Eastern Standard Time (America/Indiana/Petersburg)' }, { code: 'America/Indiana/Vincennes', name: 'Eastern Standard Time (America/Indiana/Vincennes)' }, { code: 'America/Indiana/Winamac', name: 'Eastern Standard Time (America/Indiana/Winamac)' }, { code: 'America/Iqaluit', name: 'Eastern Standard Time (America/Iqaluit)' }, { code: 'America/Kentucky/Monticello', name: 'Eastern Standard Time (America/Kentucky/Monticello)' }, { code: 'America/Louisville', name: 'Eastern Standard Time (America/Louisville)' }, { code: 'America/Montreal', name: 'Eastern Standard Time (America/Montreal)' }, { code: 'America/Nassau', name: 'Eastern Standard Time (America/Nassau)' }, { code: 'America/New_York', name: 'Eastern Standard Time (America/New_York)' }, { code: 'America/Nipigon', name: 'Eastern Standard Time (America/Nipigon)' }, { code: 'America/Pangnirtung', name: 'Eastern Standard Time (America/Pangnirtung)' }, { code: 'America/Port-au-Prince', name: 'Eastern Standard Time (America/Port-au-Prince)' }, { code: 'America/Thunder_Bay', name: 'Eastern Standard Time (America/Thunder_Bay)' }, { code: 'America/Toronto', name: 'Eastern Standard Time (America/Toronto)' }, { code: 'EST5EDT', name: 'Eastern Standard Time (EST5EDT)' }, { code: 'Africa/Cairo', name: 'Egypt Standard Time (Africa/Cairo)' }, { code: 'Asia/Yekaterinburg', name: 'Ekaterinburg Standard Time (Asia/Yekaterinburg)' }, { code: 'Europe/Helsinki', name: 'FLE Standard Time (Europe/Helsinki)' }, { code: 'Europe/Kiev', name: 'FLE Standard Time (Europe/Kiev)' }, { code: 'Europe/Mariehamn', name: 'FLE Standard Time (Europe/Mariehamn)' }, { code: 'Europe/Riga', name: 'FLE Standard Time (Europe/Riga)' }, { code: 'Europe/Simferopol', name: 'FLE Standard Time (Europe/Simferopol)' }, { code: 'Europe/Sofia', name: 'FLE Standard Time (Europe/Sofia)' }, { code: 'Europe/Tallinn', name: 'FLE Standard Time (Europe/Tallinn)' }, { code: 'Europe/Uzhgorod', name: 'FLE Standard Time (Europe/Uzhgorod)' }, { code: 'Europe/Vilnius', name: 'FLE Standard Time (Europe/Vilnius)' }, { code: 'Europe/Zaporozhye', name: 'FLE Standard Time (Europe/Zaporozhye)' }, { code: 'Pacific/Fiji', name: 'Fiji Standard Time (Pacific/Fiji)' }, { code: 'Atlantic/Canary', name: 'GMT Standard Time (Atlantic/Canary)' }, { code: 'Atlantic/Faeroe', name: 'GMT Standard Time (Atlantic/Faeroe)' }, { code: 'Atlantic/Madeira', name: 'GMT Standard Time (Atlantic/Madeira)' }, { code: 'Europe/Dublin', name: 'GMT Standard Time (Europe/Dublin)' }, { code: 'Europe/Guernsey', name: 'GMT Standard Time (Europe/Guernsey)' }, { code: 'Europe/Isle_of_Man', name: 'GMT Standard Time (Europe/Isle_of_Man)' }, { code: 'Europe/Jersey', name: 'GMT Standard Time (Europe/Jersey)' }, { code: 'Europe/Lisbon', name: 'GMT Standard Time (Europe/Lisbon)' }, { code: 'Europe/London', name: 'GMT Standard Time (Europe/London)' }, { code: 'Asia/Nicosia', name: 'GTB Standard Time (Asia/Nicosia)' }, { code: 'Europe/Athens', name: 'GTB Standard Time (Europe/Athens)' }, { code: 'Europe/Bucharest', name: 'GTB Standard Time (Europe/Bucharest)' }, { code: 'Europe/Chisinau', name: 'GTB Standard Time (Europe/Chisinau)' }, { code: 'Asia/Tbilisi', name: 'Georgian Standard Time (Asia/Tbilisi)' }, { code: 'America/Godthab', name: 'Greenland Standard Time (America/Godthab)' }, { code: 'Africa/Abidjan', name: 'Greenwich Standard Time (Africa/Abidjan)' }, { code: 'Africa/Accra', name: 'Greenwich Standard Time (Africa/Accra)' }, { code: 'Africa/Bamako', name: 'Greenwich Standard Time (Africa/Bamako)' }, { code: 'Africa/Banjul', name: 'Greenwich Standard Time (Africa/Banjul)' }, { code: 'Africa/Bissau', name: 'Greenwich Standard Time (Africa/Bissau)' }, { code: 'Africa/Conakry', name: 'Greenwich Standard Time (Africa/Conakry)' }, { code: 'Africa/Dakar', name: 'Greenwich Standard Time (Africa/Dakar)' }, { code: 'Africa/Freetown', name: 'Greenwich Standard Time (Africa/Freetown)' }, { code: 'Africa/Lome', name: 'Greenwich Standard Time (Africa/Lome)' }, { code: 'Africa/Monrovia', name: 'Greenwich Standard Time (Africa/Monrovia)' }, { code: 'Africa/Nouakchott', name: 'Greenwich Standard Time (Africa/Nouakchott)' }, { code: 'Africa/Ouagadougou', name: 'Greenwich Standard Time (Africa/Ouagadougou)' }, { code: 'Africa/Sao_Tome', name: 'Greenwich Standard Time (Africa/Sao_Tome)' }, { code: 'Atlantic/Reykjavik', name: 'Greenwich Standard Time (Atlantic/Reykjavik)' }, { code: 'Atlantic/St_Helena', name: 'Greenwich Standard Time (Atlantic/St_Helena)' }, { code: 'Etc/GMT+10', name: 'Hawaiian Standard Time (Etc/GMT+10)' }, { code: 'Pacific/Honolulu', name: 'Hawaiian Standard Time (Pacific/Honolulu)' }, { code: 'Pacific/Johnston', name: 'Hawaiian Standard Time (Pacific/Johnston)' }, { code: 'Pacific/Rarotonga', name: 'Hawaiian Standard Time (Pacific/Rarotonga)' }, { code: 'Pacific/Tahiti', name: 'Hawaiian Standard Time (Pacific/Tahiti)' }, { code: 'Asia/Calcutta', name: 'India Standard Time (Asia/Calcutta)' }, { code: 'Asia/Tehran', name: 'Iran Standard Time (Asia/Tehran)' }, { code: 'Asia/Jerusalem', name: 'Israel Standard Time (Asia/Jerusalem)' }, { code: 'Asia/Amman', name: 'Jordan Standard Time (Asia/Amman)' }, { code: 'Europe/Kaliningrad', name: 'Kaliningrad Standard Time (Europe/Kaliningrad)' }, { code: 'Europe/Minsk', name: 'Kaliningrad Standard Time (Europe/Minsk)' }, { code: 'Asia/Pyongyang', name: 'Korea Standard Time (Asia/Pyongyang)' }, { code: 'Asia/Seoul', name: 'Korea Standard Time (Asia/Seoul)' }, { code: 'Africa/Tripoli', name: 'Libya Standard Time (Africa/Tripoli)' }, { code: 'Asia/Anadyr', name: 'Magadan Standard Time (Asia/Anadyr)' }, { code: 'Asia/Kamchatka', name: 'Magadan Standard Time (Asia/Kamchatka)' }, { code: 'Asia/Magadan', name: 'Magadan Standard Time (Asia/Magadan)' }, { code: 'Indian/Mahe', name: 'Mauritius Standard Time (Indian/Mahe)' }, { code: 'Indian/Mauritius', name: 'Mauritius Standard Time (Indian/Mauritius)' }, { code: 'Indian/Reunion', name: 'Mauritius Standard Time (Indian/Reunion)' }, { code: 'Asia/Beirut', name: 'Middle East Standard Time (Asia/Beirut)' }, { code: 'America/Montevideo', name: 'Montevideo Standard Time (America/Montevideo)' }, { code: 'Africa/Casablanca', name: 'Morocco Standard Time (Africa/Casablanca)' }, { code: 'Africa/El_Aaiun', name: 'Morocco Standard Time (Africa/El_Aaiun)' }, { code: 'America/Boise', name: 'Mountain Standard Time (America/Boise)' }, { code: 'America/Cambridge_Bay', name: 'Mountain Standard Time (America/Cambridge_Bay)' }, { code: 'America/Denver', name: 'Mountain Standard Time (America/Denver)' }, { code: 'America/Edmonton', name: 'Mountain Standard Time (America/Edmonton)' }, { code: 'America/Inuvik', name: 'Mountain Standard Time (America/Inuvik)' }, { code: 'America/Ojinaga', name: 'Mountain Standard Time (America/Ojinaga)' }, { code: 'America/Shiprock', name: 'Mountain Standard Time (America/Shiprock)' }, { code: 'America/Yellowknife', name: 'Mountain Standard Time (America/Yellowknife)' }, { code: 'MST7MDT', name: 'Mountain Standard Time (MST7MDT)' }, { code: 'America/Chihuahua', name: 'Mountain Standard Time (Mexico) (America/Chihuahua)' }, { code: 'America/Mazatlan', name: 'Mountain Standard Time (Mexico) (America/Mazatlan)' }, { code: 'Asia/Rangoon', name: 'Myanmar Standard Time (Asia/Rangoon)' }, { code: 'Indian/Cocos', name: 'Myanmar Standard Time (Indian/Cocos)' }, { code: 'Asia/Novokuznetsk', name: 'N. Central Asia Standard Time (Asia/Novokuznetsk)' }, { code: 'Asia/Novosibirsk', name: 'N. Central Asia Standard Time (Asia/Novosibirsk)' }, { code: 'Asia/Omsk', name: 'N. Central Asia Standard Time (Asia/Omsk)' }, { code: 'Africa/Windhoek', name: 'Namibia Standard Time (Africa/Windhoek)' }, { code: 'Asia/Katmandu', name: 'Nepal Standard Time (Asia/Katmandu)' }, { code: 'Antarctica/McMurdo', name: 'New Zealand Standard Time (Antarctica/McMurdo)' }, { code: 'Antarctica/South_Pole', name: 'New Zealand Standard Time (Antarctica/South_Pole)' }, { code: 'Pacific/Auckland', name: 'New Zealand Standard Time (Pacific/Auckland)' }, { code: 'America/St_Johns', name: 'Newfoundland Standard Time (America/St_Johns)' }, { code: 'Asia/Irkutsk', name: 'North Asia East Standard Time (Asia/Irkutsk)' }, { code: 'Asia/Krasnoyarsk', name: 'North Asia Standard Time (Asia/Krasnoyarsk)' }, { code: 'America/Santiago', name: 'Pacific SA Standard Time (America/Santiago)' }, { code: 'Antarctica/Palmer', name: 'Pacific SA Standard Time (Antarctica/Palmer)' }, { code: 'America/Dawson', name: 'Pacific Standard Time (America/Dawson)' }, { code: 'America/Los_Angeles', name: 'Pacific Standard Time (America/Los_Angeles)' }, { code: 'America/Tijuana', name: 'Pacific Standard Time (America/Tijuana)' }, { code: 'America/Vancouver', name: 'Pacific Standard Time (America/Vancouver)' }, { code: 'America/Whitehorse', name: 'Pacific Standard Time (America/Whitehorse)' }, { code: 'America/Santa_Isabel', name: 'Pacific Standard Time (Mexico) (America/Santa_Isabel)' }, { code: 'PST8PDT', name: 'Pacific Standard Time (PST8PDT)' }, { code: 'Asia/Karachi', name: 'Pakistan Standard Time (Asia/Karachi)' }, { code: 'America/Asuncion', name: 'Paraguay Standard Time (America/Asuncion)' }, { code: 'Africa/Ceuta', name: 'Romance Standard Time (Africa/Ceuta)' }, { code: 'Europe/Brussels', name: 'Romance Standard Time (Europe/Brussels)' }, { code: 'Europe/Copenhagen', name: 'Romance Standard Time (Europe/Copenhagen)' }, { code: 'Europe/Madrid', name: 'Romance Standard Time (Europe/Madrid)' }, { code: 'Europe/Paris', name: 'Romance Standard Time (Europe/Paris)' }, { code: 'Europe/Moscow', name: 'Russian Standard Time (Europe/Moscow)' }, { code: 'Europe/Samara', name: 'Russian Standard Time (Europe/Samara)' }, { code: 'Europe/Volgograd', name: 'Russian Standard Time (Europe/Volgograd)' }, { code: 'America/Araguaina', name: 'SA Eastern Standard Time (America/Araguaina)' }, { code: 'America/Belem', name: 'SA Eastern Standard Time (America/Belem)' }, { code: 'America/Cayenne', name: 'SA Eastern Standard Time (America/Cayenne)' }, { code: 'America/Fortaleza', name: 'SA Eastern Standard Time (America/Fortaleza)' }, { code: 'America/Maceio', name: 'SA Eastern Standard Time (America/Maceio)' }, { code: 'America/Paramaribo', name: 'SA Eastern Standard Time (America/Paramaribo)' }, { code: 'America/Recife', name: 'SA Eastern Standard Time (America/Recife)' }, { code: 'America/Santarem', name: 'SA Eastern Standard Time (America/Santarem)' }, { code: 'Antarctica/Rothera', name: 'SA Eastern Standard Time (Antarctica/Rothera)' }, { code: 'Atlantic/Stanley', name: 'SA Eastern Standard Time (Atlantic/Stanley)' }, { code: 'Etc/GMT+3', name: 'SA Eastern Standard Time (Etc/GMT+3)' }, { code: 'America/Bogota', name: 'SA Pacific Standard Time (America/Bogota)' }, { code: 'America/Cayman', name: 'SA Pacific Standard Time (America/Cayman)' }, { code: 'America/Coral_Harbour', name: 'SA Pacific Standard Time (America/Coral_Harbour)' }, { code: 'America/Eirunepe', name: 'SA Pacific Standard Time (America/Eirunepe)' }, { code: 'America/Guayaquil', name: 'SA Pacific Standard Time (America/Guayaquil)' }, { code: 'America/Jamaica', name: 'SA Pacific Standard Time (America/Jamaica)' }, { code: 'America/Lima', name: 'SA Pacific Standard Time (America/Lima)' }, { code: 'America/Panama', name: 'SA Pacific Standard Time (America/Panama)' }, { code: 'America/Rio_Branco', name: 'SA Pacific Standard Time (America/Rio_Branco)' }, { code: 'Etc/GMT+5', name: 'SA Pacific Standard Time (Etc/GMT+5)' }, { code: 'America/Anguilla', name: 'SA Western Standard Time (America/Anguilla)' }, { code: 'America/Antigua', name: 'SA Western Standard Time (America/Antigua)' }, { code: 'America/Aruba', name: 'SA Western Standard Time (America/Aruba)' }, { code: 'America/Barbados', name: 'SA Western Standard Time (America/Barbados)' }, { code: 'America/Blanc-Sablon', name: 'SA Western Standard Time (America/Blanc-Sablon)' }, { code: 'America/Boa_Vista', name: 'SA Western Standard Time (America/Boa_Vista)' }, { code: 'America/Curacao', name: 'SA Western Standard Time (America/Curacao)' }, { code: 'America/Dominica', name: 'SA Western Standard Time (America/Dominica)' }, { code: 'America/Grenada', name: 'SA Western Standard Time (America/Grenada)' }, { code: 'America/Guadeloupe', name: 'SA Western Standard Time (America/Guadeloupe)' }, { code: 'America/Guyana', name: 'SA Western Standard Time (America/Guyana)' }, { code: 'America/Kralendijk', name: 'SA Western Standard Time (America/Kralendijk)' }, { code: 'America/La_Paz', name: 'SA Western Standard Time (America/La_Paz)' }, { code: 'America/Lower_Princes', name: 'SA Western Standard Time (America/Lower_Princes)' }, { code: 'America/Manaus', name: 'SA Western Standard Time (America/Manaus)' }, { code: 'America/Marigot', name: 'SA Western Standard Time (America/Marigot)' }, { code: 'America/Martinique', name: 'SA Western Standard Time (America/Martinique)' }, { code: 'America/Montserrat', name: 'SA Western Standard Time (America/Montserrat)' }, { code: 'America/Port_of_Spain', name: 'SA Western Standard Time (America/Port_of_Spain)' }, { code: 'America/Porto_Velho', name: 'SA Western Standard Time (America/Porto_Velho)' }, { code: 'America/Puerto_Rico', name: 'SA Western Standard Time (America/Puerto_Rico)' }, { code: 'America/Santo_Domingo', name: 'SA Western Standard Time (America/Santo_Domingo)' }, { code: 'America/St_Barthelemy', name: 'SA Western Standard Time (America/St_Barthelemy)' }, { code: 'America/St_Kitts', name: 'SA Western Standard Time (America/St_Kitts)' }, { code: 'America/St_Lucia', name: 'SA Western Standard Time (America/St_Lucia)' }, { code: 'America/St_Thomas', name: 'SA Western Standard Time (America/St_Thomas)' }, { code: 'America/St_Vincent', name: 'SA Western Standard Time (America/St_Vincent)' }, { code: 'America/Tortola', name: 'SA Western Standard Time (America/Tortola)' }, { code: 'Etc/GMT+4', name: 'SA Western Standard Time (Etc/GMT+4)' }, { code: 'Antarctica/Davis', name: 'SE Asia Standard Time (Antarctica/Davis)' }, { code: 'Asia/Bangkok', name: 'SE Asia Standard Time (Asia/Bangkok)' }, { code: 'Asia/Hovd', name: 'SE Asia Standard Time (Asia/Hovd)' }, { code: 'Asia/Jakarta', name: 'SE Asia Standard Time (Asia/Jakarta)' }, { code: 'Asia/Phnom_Penh', name: 'SE Asia Standard Time (Asia/Phnom_Penh)' }, { code: 'Asia/Pontianak', name: 'SE Asia Standard Time (Asia/Pontianak)' }, { code: 'Asia/Saigon', name: 'SE Asia Standard Time (Asia/Saigon)' }, { code: 'Asia/Vientiane', name: 'SE Asia Standard Time (Asia/Vientiane)' }, { code: 'Etc/GMT-7', name: 'SE Asia Standard Time (Etc/GMT-7)' }, { code: 'Indian/Christmas', name: 'SE Asia Standard Time (Indian/Christmas)' }, { code: 'Pacific/Apia', name: 'Samoa Standard Time (Pacific/Apia)' }, { code: 'Asia/Brunei', name: 'Singapore Standard Time (Asia/Brunei)' }, { code: 'Asia/Kuala_Lumpur', name: 'Singapore Standard Time (Asia/Kuala_Lumpur)' }, { code: 'Asia/Kuching', name: 'Singapore Standard Time (Asia/Kuching)' }, { code: 'Asia/Makassar', name: 'Singapore Standard Time (Asia/Makassar)' }, { code: 'Asia/Manila', name: 'Singapore Standard Time (Asia/Manila)' }, { code: 'Asia/Singapore', name: 'Singapore Standard Time (Asia/Singapore)' }, { code: 'Etc/GMT-8', name: 'Singapore Standard Time (Etc/GMT-8)' }, { code: 'Africa/Blantyre', name: 'South Africa Standard Time (Africa/Blantyre)' }, { code: 'Africa/Bujumbura', name: 'South Africa Standard Time (Africa/Bujumbura)' }, { code: 'Africa/Gaborone', name: 'South Africa Standard Time (Africa/Gaborone)' }, { code: 'Africa/Harare', name: 'South Africa Standard Time (Africa/Harare)' }, { code: 'Africa/Johannesburg', name: 'South Africa Standard Time (Africa/Johannesburg)' }, { code: 'Africa/Kigali', name: 'South Africa Standard Time (Africa/Kigali)' }, { code: 'Africa/Lubumbashi', name: 'South Africa Standard Time (Africa/Lubumbashi)' }, { code: 'Africa/Lusaka', name: 'South Africa Standard Time (Africa/Lusaka)' }, { code: 'Africa/Maputo', name: 'South Africa Standard Time (Africa/Maputo)' }, { code: 'Africa/Maseru', name: 'South Africa Standard Time (Africa/Maseru)' }, { code: 'Africa/Mbabane', name: 'South Africa Standard Time (Africa/Mbabane)' }, { code: 'Etc/GMT-2', name: 'South Africa Standard Time (Etc/GMT-2)' }, { code: 'Asia/Colombo', name: 'Sri Lanka Standard Time (Asia/Colombo)' }, { code: 'Asia/Damascus', name: 'Syria Standard Time (Asia/Damascus)' }, { code: 'Asia/Taipei', name: 'Taipei Standard Time (Asia/Taipei)' }, { code: 'Australia/Currie', name: 'Tasmania Standard Time (Australia/Currie)' }, { code: 'Australia/Hobart', name: 'Tasmania Standard Time (Australia/Hobart)' }, { code: 'Asia/Dili', name: 'Tokyo Standard Time (Asia/Dili)' }, { code: 'Asia/Jayapura', name: 'Tokyo Standard Time (Asia/Jayapura)' }, { code: 'Asia/Tokyo', name: 'Tokyo Standard Time (Asia/Tokyo)' }, { code: 'Etc/GMT-9', name: 'Tokyo Standard Time (Etc/GMT-9)' }, { code: 'Pacific/Palau', name: 'Tokyo Standard Time (Pacific/Palau)' }, { code: 'Etc/GMT-13', name: 'Tonga Standard Time (Etc/GMT-13)' }, { code: 'Pacific/Enderbury', name: 'Tonga Standard Time (Pacific/Enderbury)' }, { code: 'Pacific/Fakaofo', name: 'Tonga Standard Time (Pacific/Fakaofo)' }, { code: 'Pacific/Tongatapu', name: 'Tonga Standard Time (Pacific/Tongatapu)' }, { code: 'Europe/Istanbul', name: 'Turkey Standard Time (Europe/Istanbul)' }, { code: 'America/Indiana/Marengo', name: 'US Eastern Standard Time (America/Indiana/Marengo)' }, { code: 'America/Indiana/Vevay', name: 'US Eastern Standard Time (America/Indiana/Vevay)' }, { code: 'America/Indianapolis', name: 'US Eastern Standard Time (America/Indianapolis)' }, { code: 'America/Creston', name: 'US Mountain Standard Time (America/Creston)' }, { code: 'America/Dawson_Creek', name: 'US Mountain Standard Time (America/Dawson_Creek)' }, { code: 'America/Hermosillo', name: 'US Mountain Standard Time (America/Hermosillo)' }, { code: 'America/Phoenix', name: 'US Mountain Standard Time (America/Phoenix)' }, { code: 'Etc/GMT+7', name: 'US Mountain Standard Time (Etc/GMT+7)' }, { code: 'America/Danmarkshavn', name: 'UTC (America/Danmarkshavn)' }, { code: 'Etc/GMT', name: 'UTC (Etc/GMT)' }, { code: 'Etc/GMT-12', name: 'UTC+12 (Etc/GMT-12)' }, { code: 'Pacific/Funafuti', name: 'UTC+12 (Pacific/Funafuti)' }, { code: 'Pacific/Kwajalein', name: 'UTC+12 (Pacific/Kwajalein)' }, { code: 'Pacific/Majuro', name: 'UTC+12 (Pacific/Majuro)' }, { code: 'Pacific/Nauru', name: 'UTC+12 (Pacific/Nauru)' }, { code: 'Pacific/Tarawa', name: 'UTC+12 (Pacific/Tarawa)' }, { code: 'Pacific/Wake', name: 'UTC+12 (Pacific/Wake)' }, { code: 'Pacific/Wallis', name: 'UTC+12 (Pacific/Wallis)' }, { code: 'America/Noronha', name: 'UTC-02 (America/Noronha)' }, { code: 'Atlantic/South_Georgia', name: 'UTC-02 (Atlantic/South_Georgia)' }, { code: 'Etc/GMT+2', name: 'UTC-02 (Etc/GMT+2)' }, { code: 'Etc/GMT+11', name: 'UTC-11 (Etc/GMT+11)' }, { code: 'Pacific/Midway', name: 'UTC-11 (Pacific/Midway)' }, { code: 'Pacific/Niue', name: 'UTC-11 (Pacific/Niue)' }, { code: 'Pacific/Pago_Pago', name: 'UTC-11 (Pacific/Pago_Pago)' }, { code: 'Asia/Choibalsan', name: 'Ulaanbaatar Standard Time (Asia/Choibalsan)' }, { code: 'Asia/Ulaanbaatar', name: 'Ulaanbaatar Standard Time (Asia/Ulaanbaatar)' }, { code: 'America/Caracas', name: 'Venezuela Standard Time (America/Caracas)' }, { code: 'Asia/Sakhalin', name: 'Vladivostok Standard Time (Asia/Sakhalin)' }, { code: 'Asia/Ust-Nera', name: 'Vladivostok Standard Time (Asia/Ust-Nera)' }, { code: 'Asia/Vladivostok', name: 'Vladivostok Standard Time (Asia/Vladivostok)' }, { code: 'Antarctica/Casey', name: 'W. Australia Standard Time (Antarctica/Casey)' }, { code: 'Australia/Perth', name: 'W. Australia Standard Time (Australia/Perth)' }, { code: 'Africa/Algiers', name: 'W. Central Africa Standard Time (Africa/Algiers)' }, { code: 'Africa/Bangui', name: 'W. Central Africa Standard Time (Africa/Bangui)' }, { code: 'Africa/Brazzaville', name: 'W. Central Africa Standard Time (Africa/Brazzaville)' }, { code: 'Africa/Douala', name: 'W. Central Africa Standard Time (Africa/Douala)' }, { code: 'Africa/Kinshasa', name: 'W. Central Africa Standard Time (Africa/Kinshasa)' }, { code: 'Africa/Lagos', name: 'W. Central Africa Standard Time (Africa/Lagos)' }, { code: 'Africa/Libreville', name: 'W. Central Africa Standard Time (Africa/Libreville)' }, { code: 'Africa/Luanda', name: 'W. Central Africa Standard Time (Africa/Luanda)' }, { code: 'Africa/Malabo', name: 'W. Central Africa Standard Time (Africa/Malabo)' }, { code: 'Africa/Ndjamena', name: 'W. Central Africa Standard Time (Africa/Ndjamena)' }, { code: 'Africa/Niamey', name: 'W. Central Africa Standard Time (Africa/Niamey)' }, { code: 'Africa/Porto-Novo', name: 'W. Central Africa Standard Time (Africa/Porto-Novo)' }, { code: 'Africa/Tunis', name: 'W. Central Africa Standard Time (Africa/Tunis)' }, { code: 'Etc/GMT-1', name: 'W. Central Africa Standard Time (Etc/GMT-1)' }, { code: 'Arctic/Longyearbyen', name: 'W. Europe Standard Time (Arctic/Longyearbyen)' }, { code: 'Europe/Amsterdam', name: 'W. Europe Standard Time (Europe/Amsterdam)' }, { code: 'Europe/Andorra', name: 'W. Europe Standard Time (Europe/Andorra)' }, { code: 'Europe/Berlin', name: 'W. Europe Standard Time (Europe/Berlin)' }, { code: 'Europe/Busingen', name: 'W. Europe Standard Time (Europe/Busingen)' }, { code: 'Europe/Gibraltar', name: 'W. Europe Standard Time (Europe/Gibraltar)' }, { code: 'Europe/Luxembourg', name: 'W. Europe Standard Time (Europe/Luxembourg)' }, { code: 'Europe/Malta', name: 'W. Europe Standard Time (Europe/Malta)' }, { code: 'Europe/Monaco', name: 'W. Europe Standard Time (Europe/Monaco)' }, { code: 'Europe/Oslo', name: 'W. Europe Standard Time (Europe/Oslo)' }, { code: 'Europe/Rome', name: 'W. Europe Standard Time (Europe/Rome)' }, { code: 'Europe/San_Marino', name: 'W. Europe Standard Time (Europe/San_Marino)' }, { code: 'Europe/Stockholm', name: 'W. Europe Standard Time (Europe/Stockholm)' }, { code: 'Europe/Vaduz', name: 'W. Europe Standard Time (Europe/Vaduz)' }, { code: 'Europe/Vatican', name: 'W. Europe Standard Time (Europe/Vatican)' }, { code: 'Europe/Vienna', name: 'W. Europe Standard Time (Europe/Vienna)' }, { code: 'Europe/Zurich', name: 'W. Europe Standard Time (Europe/Zurich)' }, { code: 'Antarctica/Mawson', name: 'West Asia Standard Time (Antarctica/Mawson)' }, { code: 'Asia/Aqtau', name: 'West Asia Standard Time (Asia/Aqtau)' }, { code: 'Asia/Aqtobe', name: 'West Asia Standard Time (Asia/Aqtobe)' }, { code: 'Asia/Ashgabat', name: 'West Asia Standard Time (Asia/Ashgabat)' }, { code: 'Asia/Dushanbe', name: 'West Asia Standard Time (Asia/Dushanbe)' }, { code: 'Asia/Oral', name: 'West Asia Standard Time (Asia/Oral)' }, { code: 'Asia/Samarkand', name: 'West Asia Standard Time (Asia/Samarkand)' }, { code: 'Asia/Tashkent', name: 'West Asia Standard Time (Asia/Tashkent)' }, { code: 'Etc/GMT-5', name: 'West Asia Standard Time (Etc/GMT-5)' }, { code: 'Indian/Kerguelen', name: 'West Asia Standard Time (Indian/Kerguelen)' }, { code: 'Indian/Maldives', name: 'West Asia Standard Time (Indian/Maldives)' }, { code: 'Antarctica/DumontDUrville', name: 'West Pacific Standard Time (Antarctica/DumontDUrville)' }, { code: 'Etc/GMT-10', name: 'West Pacific Standard Time (Etc/GMT-10)' }, { code: 'Pacific/Guam', name: 'West Pacific Standard Time (Pacific/Guam)' }, { code: 'Pacific/Port_Moresby', name: 'West Pacific Standard Time (Pacific/Port_Moresby)' }, { code: 'Pacific/Saipan', name: 'West Pacific Standard Time (Pacific/Saipan)' }, { code: 'Pacific/Truk', name: 'West Pacific Standard Time (Pacific/Truk)' }, { code: 'Asia/Khandyga', name: 'Yakutsk Standard Time (Asia/Khandyga)' }, { code: 'Asia/Yakutsk', name: 'Yakutsk Standard Time (Asia/Yakutsk)' } ]; ================================================ FILE: packages/evershop/src/lib/locale/translate/_.ts ================================================ export function _(text: string, values?: Record): string { // Check if the data is null, undefined or empty object if (!values || Object.keys(values).length === 0) { return text; } const template = `${text}`; return template.replace(/\${(.*?)}/g, (match, key) => values[key.trim()] !== undefined ? values[key.trim()] : match ); } ================================================ FILE: packages/evershop/src/lib/locale/translate/translate.ts ================================================ import { loadCsvTranslationFiles } from '../../webpack/loaders/loadTranslationFromCsv.js'; let csvData: Record | undefined; /** * This function is used to translate the text form server side, like from middleware. For templating use the _ function */ export function translate(enText: string, values: Record = {}) { const translatedText = csvData && csvData[enText] !== undefined ? csvData[enText] : enText; // Check if the data is null, undefined or empty object if (!values || Object.keys(values).length === 0) { return translatedText; } else { const template = `${translatedText}`; return template.replace(/\${(.*?)}/g, (match, key) => values[key.trim()] !== undefined ? values[key.trim()] : match ); } } export async function loadCsv(): Promise> { // Only load the csv files once if (csvData === undefined) { csvData = await loadCsvTranslationFiles(); } return csvData; } ================================================ FILE: packages/evershop/src/lib/log/CustomColorize.js ================================================ // Extend the default winston colorize format and add color to the stack trace import { LEVEL } from 'triple-beam'; import { format } from 'winston'; const { colorize } = format; class CustomColorize extends colorize.Colorizer { constructor(opts = {}) { super(opts); } transform(info, opts) { super.transform(info, opts); if (info.stack) { info.stack = this.colorize(info[LEVEL], info.level, info.stack); } return info; } } export default (opts) => new CustomColorize(opts); ================================================ FILE: packages/evershop/src/lib/log/logger.js ================================================ import winston from 'winston'; import { getEnv } from '../util/getEnv.js'; import isDevelopmentMode from '../util/isDevelopmentMode.js'; import { addProcessor, getValueSync } from '../util/registry.js'; import CustomColorize from './CustomColorize.js'; function createLogger() { return getValueSync('logger', null, { isDebugging: isDevelopmentMode() || process.argv.includes('--debug') }); } // Define logger function export function debug(message) { const logger = createLogger(); logger.debug(message); } export function error(e) { const logger = createLogger(); logger.error(e); } export function warning(message) { const logger = createLogger(); logger.warn(message); } export function info(message) { const logger = createLogger(); logger.info(message); } export function success(message) { const logger = createLogger(); logger.info(message); } addProcessor( 'logger', () => { const config = getValueSync( 'logger_configuration', () => { const { errors } = winston.format; const isDebugging = isDevelopmentMode() || process.argv.includes('--debug'); const format = winston.format.combine( errors({ stack: true }), CustomColorize({ colors: { error: 'red', warn: 'yellow', info: 'green', http: 'blue', verbose: 'cyan', debug: 'magenta', silly: 'gray' }, level: false, message: true }), winston.format.printf(({ level, message, stack }) => { let icon; switch (level) { case 'error': icon = '❌'; // Error icon break; case 'warn': icon = '⚠️ '; // Warning icon break; case 'info': icon = 'ℹ️'; // Info icon break; case 'http': icon = '🌐'; // HTTP icon break; case 'verbose': icon = '🔍'; // Verbose icon break; case 'debug': icon = '🐛'; // Debug icon break; case 'silly': icon = '🤪'; // Silly icon break; default: icon = ''; break; } // Now apply color to the icon and level switch (level) { case 'error': level = `\x1b[31m${level}\x1b[0m`; // Red color icon = `\x1b[31m${icon}\x1b[0m`; // Red color break; case 'warn': level = `\x1b[33m${level}\x1b[0m`; // Yellow color icon = `\x1b[33m${icon}\x1b[0m`; // Yellow color break; case 'info': level = `\x1b[32m${level}\x1b[0m`; // Green color icon = `\x1b[32m${icon}\x1b[0m`; // Green color break; case 'http': level = `\x1b[34m${level}\x1b[0m`; // Blue color icon = `\x1b[34m${icon}\x1b[0m`; // Blue color break; case 'verbose': level = `\x1b[36m${level}\x1b[0m`; // Cyan color icon = `\x1b[36m${icon}\x1b[0m`; // Cyan color break; case 'debug': level = `\x1b[35m${level}\x1b[0m`; // Magenta color icon = `\x1b[35m${icon}\x1b[0m`; // Magenta color break; case 'silly': level = `\x1b[37m${level}\x1b[0m`; // Gray color icon = `\x1b[37m${icon}\x1b[0m`; // Gray color break; default: break; } if (stack) { message = `${message}\n${stack}`; } return `${icon} ${level}: \n${message}`; }) ); const consoleTransport = new winston.transports.Console(); const logFile = getEnv('LOG_FILE', undefined); // Default transports const DEFAULT_CONFIG = { level: isDebugging ? 'silly' : getEnv('LOGGER_LEVEL', 'warn'), format, // By default, log to console transports: isDebugging || !logFile ? [consoleTransport] : [new winston.transports.File({ filename: logFile })], exceptionHandlers: isDebugging || !logFile ? [consoleTransport] : [new winston.transports.File({ filename: logFile })] }; return DEFAULT_CONFIG; }, { isDebugging: isDevelopmentMode() || process.argv.includes('--debug') } ); return winston.createLogger(config); }, 0 ); ================================================ FILE: packages/evershop/src/lib/mail/emailHelper.ts ================================================ import Handlebars from 'handlebars'; import { getSetting } from '../../modules/setting/services/setting.js'; import { countries } from '../locale/countries.js'; import { provinces } from '../locale/provinces.js'; import { getBaseUrl } from '../util/getBaseUrl.js'; import { getConfig } from '../util/getConfig.js'; import { addProcessor, getValue, getValueSync } from '../util/registry.js'; Handlebars.registerHelper('currency', function (value) { if (value == null) return ''; const number = Number(value); return new Intl.NumberFormat('en-US', { style: 'currency', currency: getConfig('shop.currency', 'USD'), minimumFractionDigits: 0, maximumFractionDigits: 2 }).format(number); }); Handlebars.registerHelper('date', function (value, format = 'MMM DD, YYYY') { if (!value) return ''; let date; // handle seconds vs milliseconds if (typeof value === 'number' || /^\d+$/.test(value)) { const ts = Number(value); date = new Date(ts < 1e12 ? ts * 1000 : ts); } else { date = new Date(value); } if (isNaN(date.getTime())) return ''; return new Intl.DateTimeFormat(getConfig('shop.language', 'en'), { year: 'numeric', month: 'short', day: '2-digit' }).format(date); }); export type SendEmailArguments = { from?: string; to: string; subject: string; body?: string; template: string; data: EmailData; [key: string]: unknown; }; /** * Validates email arguments to ensure they meet the required format. * @param args - The arguments to validate * @throws Will throw an error if validation fails */ export function validateSendEmailArguments( args: unknown ): asserts args is SendEmailArguments { // Validate args is an object if (typeof args !== 'object' || args === null) { throw new Error('Email arguments must be an object.'); } const typedArgs = args as Record; // Validate required fields exist and are non-empty strings if (typeof typedArgs.to !== 'string' || typedArgs.to.trim() === '') { throw new Error('"to" field must be a non-empty string.'); } if ( typeof typedArgs.subject !== 'string' || typedArgs.subject.trim() === '' ) { throw new Error('"subject" field must be a non-empty string.'); } if ( typeof typedArgs.template !== 'string' || typedArgs.template.trim() === '' ) { throw new Error('"template" field must be a non-empty string.'); } // Body is optional, but it must be a string if provided if ( typedArgs.body !== undefined && (typeof typedArgs.body !== 'string' || typedArgs.body.trim() === '') ) { throw new Error('"body" field must be a non-empty string if provided.'); } // Validate optional fields if present if ( typedArgs.template !== undefined && typeof typedArgs.template !== 'string' ) { throw new Error('"template" field must be a string if provided.'); } if (typedArgs.cc !== undefined && !Array.isArray(typedArgs.cc)) { throw new Error('"cc" field must be an array if provided.'); } // Validate cc array contains only strings if (Array.isArray(typedArgs.cc)) { if (!typedArgs.cc.every((email) => typeof email === 'string')) { throw new Error('"cc" array must contain only strings.'); } } } export interface EmailService { sendEmail: (args: SendEmailArguments) => Promise; } /** * Validates if the given service implements the EmailService interface. * @param service - The service to validate * @returns True if valid, false otherwise */ function isValidEmailService(service: unknown): service is EmailService { return ( typeof service === 'object' && service !== null && 'sendEmail' in service && typeof (service as EmailService).sendEmail === 'function' ); } /** * Retrieves the registered email service from the registry. * @returns The email service object. */ export function getEmailService(): EmailService | undefined { const emailService = getValueSync( 'emailService', undefined, {}, isValidEmailService ); return emailService; } /** Registers a new email service. * @param service - The email service to register. * @throws Will throw an error if the service does not implement the EmailService interface. */ export function registerEmailService(service: EmailService): void { if (!isValidEmailService(service)) { throw new Error( 'Invalid email service. It must be an object with a sendEmail method.' ); } addProcessor('emailService', () => { return service; }); } /** * Sends an email using the registered email service. * @param id - The identifier for the email type, e.g., 'order_confirmation' * @param args - The email arguments * @returns A promise that resolves when the email is sent. */ export async function sendEmail( id: string, args: SendEmailArguments ): Promise { const emailService = getEmailService(); if (!emailService) { return Promise.reject( new Error('No email service registered to send emails.') ); } const finalArgs = await getValue('emailArguments', args, { id }); if (!finalArgs?.from) { finalArgs.from = getConfig('system.notification_emails.from', undefined); } validateSendEmailArguments(finalArgs); if (!finalArgs.body) { const body = await buildEmailBodyFromTemplate( finalArgs.template, finalArgs.data || {} ); finalArgs.body = body; } return await emailService.sendEmail(finalArgs); } export interface EmailData { storeInfo?: { logo?: { src?: string; alt?: string; height?: string; width?: string; }; storeName: string; storeEmail: string; storeDescription: string; phone: string; homeUrl: string; address: { country?: string; province?: string; city?: string; street?: string; postalCode?: string; }; }; [key: string]: unknown; } /** * Builds email body from a template by replacing placeholders with actual data. * @param template - The email template string with placeholders in {{key}} format. * @param data - An object containing key-value pairs to replace in the template. * @returns The final email body string with placeholders replaced by actual data. */ export async function buildEmailBodyFromTemplate( template: string, data: EmailData ): Promise { try { const preparedData = await prepareData(data); const body = Handlebars.compile(template)(preparedData); return body; } catch (error) { throw new Error(`Failed to build email body from template: ${error}`); } } /** Prepares email data by adding store information and processing through registry. * @param data - The initial email data. * @returns The prepared email data with store information. */ async function prepareData(data: EmailData): Promise { const logoConfig = getConfig('themeConfig.logo'); let logo; if (logoConfig) { const url = logoConfig.src || ''; // check if url is absolute if (url && !/^https?:\/\//i.test(url)) { logo = { src: `${getBaseUrl()}${url}`, alt: logoConfig?.alt || '', height: logoConfig?.height ? String(logoConfig.height) : undefined, width: logoConfig?.width ? String(logoConfig.width) : undefined }; } else { logo = { src: url, alt: logoConfig?.alt || '', height: logoConfig?.height ? String(logoConfig.height) : undefined, width: logoConfig?.width ? String(logoConfig.width) : undefined }; } } const addressCountry = await getSetting('storeCountry', 'US'); const addressProvince = await getSetting('storeProvince', ''); const addressCity = await getSetting('storeCity', ''); const addressStreet = await getSetting('storeAddress', ''); const addressPostalCode = await getSetting('storePostalCode', ''); const storeInformation = { logo, storeName: await getSetting('storeName', 'Evershop'), storeEmail: await getSetting('storeEmail', ''), storeDescription: await getSetting('storeDescription', ''), phone: await getSetting('storePhoneNumber', ''), homeUrl: getBaseUrl(), address: { country: countries.find((c) => c.code === addressCountry)?.name, province: provinces.find((p) => p.code === addressProvince)?.name, city: addressCity, street: addressStreet, postalCode: addressPostalCode } }; data.storeInfo = storeInformation; const finalData = await getValue('emailTemplateData', data, {}); return finalData; } ================================================ FILE: packages/evershop/src/lib/middleware/Handler.js ================================================ import { existsSync } from 'fs'; import path, { dirname } from 'path'; import { error } from '../log/logger.js'; import { getRoutes } from '../router/Router.js'; import isDevelopmentMode from '../util/isDevelopmentMode.js'; import isProductionMode from '../util/isProductionMode.js'; import isErrorHandlerTriggered from './isErrorHandlerTriggered.js'; import { noDublicateId } from './noDuplicateId.js'; import { parseFromFile } from './parseFromFile.js'; import { sortMiddlewares } from './sort.js'; export class Handler { constructor(routeId) { this.routeId = routeId; } static addMiddleware(middleware) { this.middlewares.push(middleware); } static getMiddlewares() { return this.middlewares; } static getMiddleware(id) { return this.middlewares.find((m) => m.id === id); } static getMiddlewareByRoute(route) { const routeId = route.id; if (isProductionMode() && this.sortedMiddlewarePerRoute[routeId]) { return this.sortedMiddlewarePerRoute[routeId]; } const region = route.isApi ? 'api' : 'pages'; let middlewares = this.middlewares.filter( (m) => (m.routeId === route.id || m.scope === 'app') && m.region === region ); if (route.isAdmin === true) { middlewares = middlewares.concat( this.middlewares.filter( (m) => m.routeId === 'admin' && m.region === region ) ); } else { middlewares = middlewares.concat( this.middlewares.filter( (m) => m.routeId === 'frontStore' && m.region === region ) ); } middlewares = sortMiddlewares(middlewares); if (isDevelopmentMode()) { middlewares.unshift({ middleware: (request, response, next) => { if (!existsSync(route.folder)) { response.statusCode = 404; const routes = getRoutes(); request.currentRoute = routes.find((r) => r.id === 'notFound'); } next(); } }); } this.sortedMiddlewarePerRoute[routeId] = middlewares; return middlewares; } static getAppLevelMiddlewares(region) { return sortMiddlewares( this.middlewares.filter((m) => m.scope === 'app' && m.region === region) ); } static removeMiddleware(path) { this.middlewares = this.middlewares.filter((m) => m.path !== path); } static removeMiddlewares(basePath) { this.middlewares = this.middlewares.filter((m) => { if (m.path === basePath) { return false; } else if (dirname(m.path) === basePath) { return false; } else { return true; } }); } static addMiddlewareFromPath(path) { if (!existsSync(path) || !path.endsWith('.js')) { throw new Error(`Middleware file ${path} does not exist`); } else { const middlewares = parseFromFile(path); middlewares.forEach((middleware) => { if (noDublicateId(this.middlewares, middleware)) { this.addMiddleware(middleware); } else { error(`Duplicate middleware id: ${middleware.id}`); } }); } } static middleware() { return (request, response, next) => { var _a; request.params = { ...(((_a = request.locals) === null || _a === void 0 ? void 0 : _a.customParams) || {}), ...request.params }; const { currentRoute } = request; let middlewares; if (!currentRoute) { middlewares = this.getAppLevelMiddlewares('pages'); } else { middlewares = this.getMiddlewareByRoute(currentRoute); } const goodHandlers = middlewares.filter((m) => m.middleware.length === 3); const errorHandlers = middlewares.filter( (m) => m.middleware.length === 4 ); let currentGood = 0; let currentError = -1; const eNext = function eNext() { if (arguments.length === 0 && currentGood === goodHandlers.length - 1) { next(); } else if (currentError === errorHandlers.length - 1) { next(arguments[0]); } else if (arguments.length > 0) { // Call the error handler middleware if it is not called yet if (!isErrorHandlerTriggered(response)) { currentError += 1; const middlewareFunc = errorHandlers[currentError].middleware; middlewareFunc(arguments[0], request, response, eNext); } } else { currentGood += 1; const middlewareFunc = goodHandlers[currentGood].middleware; middlewareFunc(request, response, eNext); } }; // Run the middlewares const { middleware } = goodHandlers[0]; middleware(request, response, eNext); }; } } Handler.middlewares = []; Handler.sortedMiddlewarePerRoute = {}; ================================================ FILE: packages/evershop/src/lib/middleware/addMiddleware.js ================================================ import { findDublicatedMiddleware } from './findDublicatedMiddleware.js'; import { Handler } from './Handler.js'; export function addMiddleware(middleware) { const index = findDublicatedMiddleware(Handler.middlewares, middleware); if (index === -1) { Handler.addMiddleware(middleware); } else { const addedMiddleware = Handler.middlewares[index]; throw new Error( `Found two middleware with the same id: ${middleware.path} and ${addedMiddleware.path}` ); } } ================================================ FILE: packages/evershop/src/lib/middleware/buildMiddlewareFunction.js ================================================ import { existsSync } from 'fs'; import { sep } from 'path'; import { pathToFileURL } from 'url'; import { debug, error } from '../log/logger.js'; import isDevelopmentMode from '../util/isDevelopmentMode.js'; import isProductionMode from '../util/isProductionMode.js'; import { hasDelegate, setDelegate } from './delegate.js'; import eNext from './eNext.js'; import isErrorHandlerTriggered from './isErrorHandlerTriggered.js'; /** * This function takes the defined middleware function and return a new one with wrapper * * @param {string} id * @param {function} middleware: The middleware function * @param {string} routeId: The route Id * @param {string} before: The middleware function that executes after this one * @param {string} after: The middleware function that executes before this one * @returns {object} the middleware object * @throws */ export function buildMiddlewareFunction(id, path) { if (!/^[a-zA-Z0-9_]+$/.test(id)) { throw new TypeError(`Middleware ID ${id} is invalid`); } const isRoutedLevel = !['all', 'global'].includes( path.split(sep).reverse()[1] ); // Check if the middleware is an error handler. // TODO: fix me if (id === 'errorHandler' || id === 'apiErrorHandler') { return async (error, request, response, next) => { const m = isDevelopmentMode() ? await import(`${pathToFileURL(path)}?t=${Date.now()}`) : await import(pathToFileURL(path)); const func = m.default; if (request.currentRoute) { await func(error, request, response, next); } else { await func(error, request, response, next); } }; } else { return async (request, response, next) => { const startTime = process.hrtime(); const debuging = { id }; response.debugMiddlewares.push(debuging); // If there response status is 404. We skip routed middlewares if (response.statusCode === 404 && isRoutedLevel) { next(); } else { try { const m = isDevelopmentMode() ? await import(`${pathToFileURL(path)}?t=${Date.now()}`) : await import(pathToFileURL(path)); let func = m.default; if (!func) { if (isProductionMode()) { throw new Error( `Middleware ${id} is invalid. It should provide a function as default export.` ); } else { func = () => { debug( `Middleware ${id} is not implemented yet. Please implement it.` ); }; } } if (func.length === 3) { await func(request, response, (err) => { const endTime = process.hrtime(startTime); debuging.time = endTime[1] / 1000000; eNext(request, response, next)(err); }); } else { const returnValue = await func(request, response); if (!hasDelegate(id, request)) { setDelegate(id, returnValue, request); } const endTime = process.hrtime(startTime); debuging.time = endTime[1] / 1000000; eNext(request, response, next)(); } } catch (e) { // Log the error e.message = `Exception in middleware ${id}: ${e.message}`; error(e); // Call error handler middleware if it is not called yet next(e); } } }; } } ================================================ FILE: packages/evershop/src/lib/middleware/delegate.ts ================================================ import { EvershopRequest } from '../../types/request.js'; function createWriteOnceMap() { const _map = new Map(); return { /** * Set a value once. Throws if the key already exists. */ setOnce(key: K, value: V): void { if (_map.has(key)) { throw new Error(`Key "${String(key)}" is already set.`); } _map.set(key, value); }, /** * Get a **cloned copy** of the value. This ensures that the original value * cannot be modified outside of this map. */ get(key: K): V | undefined { const val = _map.get(key); return val !== undefined ? structuredClone(val) : undefined; }, has(key: K): boolean { return _map.has(key); }, keys(): string[] { return Array.from(_map.keys()).map((key) => String(key)); }, getAll(): Record { const result: Record = {}; for (const [key, value] of _map.entries()) { result[String(key)] = structuredClone(value); } return result; } }; } /** * Retrieves the delegate manager for the given request. * @param request The request object containing the delegate manager. * @template T The type of the delegate. * @returns The delegate manager. */ export function getDelegateManager(request: EvershopRequest) { return request?.locals?.delegates || createWriteOnceMap(); } /** * Checks if a delegate exists for the given ID in the request. * @param id The delegate ID to check. * @template T The type of the delegate. * @param request The request object. * @returns True if the delegate exists, false otherwise. */ export function hasDelegate(id: string, request: EvershopRequest): boolean { return getDelegateManager(request).has(id); } /** * Retrieves a delegate value for the given ID. * @param id The delegate ID to retrieve. * @template T The type of the delegate. * @param request The request object. * @returns The delegate value or undefined if not found. */ export function getDelegate( id: string, request: EvershopRequest ): T | undefined { return getDelegateManager(request).get(id) as T | undefined; } export function getDelegates(request: EvershopRequest): Record { return getDelegateManager(request).getAll(); } /** * Sets a delegate for the given request object. * @param id The delegate ID. * @param value The delegate value. * @param request The request object. */ export function setDelegate( id: string, value: T, request: EvershopRequest ): void { if (!request.locals) { request.locals = { user: null, customer: null, context: {}, delegates: createWriteOnceMap(), sessionID: null, webpackMatchedRoute: null }; } if (!request.locals.delegates) { request.locals.delegates = createWriteOnceMap(); } request.locals.delegates.setOnce(id, value); } ================================================ FILE: packages/evershop/src/lib/middleware/eNext.js ================================================ import isErrorHandlerTriggered from './isErrorHandlerTriggered.js'; function noop() {} function eNext(request, response, next) { return (error) => { if (!isErrorHandlerTriggered(response)) { error ? next(error) : next(); } else { noop(); } }; } export default eNext; ================================================ FILE: packages/evershop/src/lib/middleware/findDublicatedMiddleware.js ================================================ export function findDublicatedMiddleware(registeredMiddlewares, newMiddleware) { return registeredMiddlewares.findIndex( (middleware) => middleware.id === newMiddleware.id && (middleware.routeId === null || middleware.scope === 'app' || newMiddleware.routeId === null || middleware.routeId === newMiddleware.routeId || (middleware.scope === newMiddleware.scope && ([middleware.routeId, newMiddleware.routeId].includes('admin') || [middleware.routeId, newMiddleware.routeId].includes( 'frontStore' )))) && middleware.region === newMiddleware.region ); } ================================================ FILE: packages/evershop/src/lib/middleware/getRouteFromPath.js ================================================ import { sep } from 'path'; export function getRouteFromPath(path) { const parts = path.split(sep).reverse(); // Check if current path is an api path if (parts[2] === 'api') { return { region: 'api', scope: parts[1] === 'global' ? 'app' : parts[1], routeId: parts[1] === 'global' ? null : parts[1] }; } // Current path is a page path let scope; let routeId; let region; region = parts[3]; if (parts[1] === 'global') { scope = 'app'; routeId = null; region = parts[2]; } else if (parts[1] === 'all' && ['frontStore', 'admin'].includes(parts[2])) { scope = routeId = parts[2]; } else if ( /^[A-Za-z+.]+$/.test(parts[1]) && ['frontStore', 'admin'].includes(parts[2]) ) { scope = parts[2]; const routes = parts[1].split('+'); if (routes.length > 1) { routeId = routes.filter((r) => r !== ''); } else { routeId = parts[1]; } } else { throw new Error(`Path ${path} is not valid for a route`); } return { region, scope, routeId }; } ================================================ FILE: packages/evershop/src/lib/middleware/index.js ================================================ import { existsSync, readdirSync } from 'fs'; import { resolve } from 'path'; import { addMiddleware } from './addMiddleware.js'; import { Handler } from './Handler.js'; import { scanForMiddlewareFunctions } from './scanForMiddlewareFunctions.js'; import { sortMiddlewares } from './sort.js'; const middlewareList = Handler.middlewares; export function getAdminMiddlewares(routeId) { return sortMiddlewares( middlewareList.filter( (m) => m.routeId === 'admin' || m.routeId === routeId || m.routeId === null ) ); } export function getFrontMiddlewares(routeId) { return sortMiddlewares( middlewareList.filter( (m) => m.routeId === 'frontStore' || m.routeId === routeId || m.routeId === null ) ); } /** * This function scan and load all middleware function of a module base on module path * * @param {string} path The path of the module * */ export function getModuleMiddlewares(path) { if (existsSync(resolve(path, 'pages'))) { // Scan for the application level middleware if (existsSync(resolve(path, 'pages', 'global'))) { scanForMiddlewareFunctions(resolve(path, 'pages', 'global')).forEach( (m) => { addMiddleware(m); } ); } // Scan for the admin level middleware if (existsSync(resolve(path, 'pages', 'admin'))) { const routes = readdirSync(resolve(path, 'pages', 'admin'), { withFileTypes: true }) .filter((dirent) => dirent.isDirectory()) .map((dirent) => dirent.name); routes.forEach((route) => { scanForMiddlewareFunctions( resolve(path, 'pages', 'admin', route) ).forEach((m) => { addMiddleware(m); }); }); } // Scan for the frontStore level middleware if (existsSync(resolve(path, 'pages', 'frontStore'))) { const routes = readdirSync(resolve(path, 'pages', 'frontStore'), { withFileTypes: true }) .filter((dirent) => dirent.isDirectory()) .map((dirent) => dirent.name); routes.forEach((route) => { scanForMiddlewareFunctions( resolve(path, 'pages', 'frontStore', route) ).forEach((m) => { addMiddleware(m); }); }); } } // Scan for the api middleware if (existsSync(resolve(path, 'api'))) { const routes = readdirSync(resolve(path, 'api'), { withFileTypes: true }) .filter((dirent) => dirent.isDirectory()) .map((dirent) => dirent.name); routes.forEach((route) => { scanForMiddlewareFunctions(resolve(path, 'api', route)).forEach((m) => { addMiddleware(m); }); }); } } /** * This function return a list of sorted middleware functions (all) * * @return {array} List of sorted middleware functions */ export function getAllSortedMiddlewares() { return sortMiddlewares(middlewareList); } ================================================ FILE: packages/evershop/src/lib/middleware/isErrorHandlerTriggered.js ================================================ export default (response) => { if (!response.locals) { return false; } else { return response.locals.errorHandlerTriggered === true; } }; ================================================ FILE: packages/evershop/src/lib/middleware/isNextRequired.js ================================================ import { readFileSync } from 'fs'; export default function isNextRequired(path) { const code = readFileSync(path, 'utf8'); return code.includes('next'); } ================================================ FILE: packages/evershop/src/lib/middleware/noDuplicateId.js ================================================ import { findDublicatedMiddleware } from './findDublicatedMiddleware.js'; /** * This function check if the new middleware function has dublicated ID or not * * @param {array} registeredMiddlewares The list of registered middleware functions * @param {object} newMiddleware The new middleware * * @return {boolean} */ export function noDublicateId(registeredMiddlewares, newMiddleware) { if (findDublicatedMiddleware(registeredMiddlewares, newMiddleware) !== -1) { return false; } else { return true; } } ================================================ FILE: packages/evershop/src/lib/middleware/parseFromFile.js ================================================ import { basename } from 'path'; import { buildMiddlewareFunction } from './buildMiddlewareFunction.js'; import { getRouteFromPath } from './getRouteFromPath.js'; export function parseFromFile(path) { const name = basename(path); let m = {}; let id; if (/^(\[)[a-zA-Z1-9.,]+(\])[a-zA-Z1-9]+.js$/.test(name)) { const split = name.split(/[\[\]]+/); id = split[2].substr(0, split[2].indexOf('.')).trim(); m = { id, middleware: buildMiddlewareFunction(id, path), after: split[1].split(',').filter((a) => a.trim() !== ''), path }; } else if (/^[a-zA-Z1-9]+(\[)[a-zA-Z1-9,]+(\]).js$/.test(name)) { const split = name.split(/[\[\]]+/); id = split[0].trim(); m = { id, middleware: buildMiddlewareFunction(id, path), before: split[1].split(',').filter((a) => a.trim() !== ''), path }; } else if ( /^(\[)[a-zA-Z1-9,]+(\])[a-zA-Z1-9]+(\[)[a-zA-Z1-9,]+(\]).js$/.test(name) ) { const split = name.split(/[\[\]]+/); id = split[2].trim(); m = { id, middleware: buildMiddlewareFunction(id, path), after: split[1].split(',').filter((a) => a.trim() !== ''), before: split[3].split(',').filter((a) => a.trim() !== ''), path }; } else { const split = name.split('.'); id = split[0].trim(); m = { id, middleware: buildMiddlewareFunction(id, path), path }; } const route = getRouteFromPath(path); if (route.region === 'api') { if (m.id !== 'context' && m.id !== 'apiErrorHandler') { m.before = !m.before ? ['apiResponse'] : m.before; m.after = !m.after ? ['escapeHtml', 'auth'] : m.after; } } else if (m.id !== 'context' && m.id !== 'errorHandler') { m.before = !m.before ? ['notFound'] : m.before; m.after = !m.after ? ['auth'] : m.after; } // Check if routeId is an array of routeIds or a single routeId if (Array.isArray(route.routeId)) { return route.routeId.map((r) => ({ ...m, region: route.region, scope: route.scope, routeId: r })); } else { return [ { ...m, ...route } ]; } } ================================================ FILE: packages/evershop/src/lib/middleware/scanForMiddlewareFunctions.js ================================================ import { readdirSync } from 'fs'; import { resolve } from 'path'; import { parseFromFile } from './parseFromFile.js'; /** * This function take a path and scan for the middleware functions * * @param {string} path The path of the folder where middleware functions are located * * @return {array} List of middleware function */ export function scanForMiddlewareFunctions(path) { let middlewares = []; readdirSync(resolve(path), { withFileTypes: true }) .filter( (dirent) => dirent.isFile() && /\.js$/.test(dirent.name) && !/^[A-Z]/.test(dirent.name[0]) ) .forEach((dirent) => { const middlewareFunc = resolve(path, dirent.name); middlewares = middlewares.concat(parseFromFile(middlewareFunc)); }); return middlewares; } ================================================ FILE: packages/evershop/src/lib/middleware/sort.js ================================================ import Topo from '@hapi/topo'; /** * This function take a path and scan for the middleware functions * * @param {array} middlewares The list of middleware functions * * @return {array} List of sorted middleware functions */ export function sortMiddlewares(middlewares = []) { const middlewareFunctions = middlewares.filter((m) => { if ((m.before === m.after) === null) return true; const dependencies = (m.before || []).concat(m.after || []); let flag = true; dependencies.forEach((d) => { if ( flag === false || middlewares.findIndex( (e) => e.id === d && (e.scope === 'app' || e.scope === 'admin' || e.scope === 'frontStore' || e.routeId === null || e.routeId === m.scope || e.routeId === m.routeId) ) === -1 ) { flag = false; } }); return flag; }); const sorter = new Topo.Sorter(); middlewareFunctions.forEach((m) => { sorter.add(m.id, { before: m.before, after: m.after, group: m.id }); }); return sorter.nodes.map((n) => { const index = middlewareFunctions.findIndex((m) => m.id === n); const m = middlewareFunctions[index]; middlewareFunctions.splice(index, 1); return m; }); } ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/app.js ================================================ import path from 'path'; import { addDefaultMiddlewareFuncs } from '../../../../bin/lib/addDefaultMiddlewareFuncs.js'; import express from 'express'; import { loadModuleRoutes } from '../../../../lib/router/loadModuleRoutes.js'; import { once } from 'events'; import { getModuleMiddlewares } from '../../index.js'; import { getRoutes } from '../../../router/Router.js'; import { Handler } from '../../Handler.js'; import { error } from '../../../log/logger.js'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** Create express app */ const app = express(); /* Loading modules and initilize routes, components and services */ const modules = [ { name: 'api', path: path.resolve(__dirname, './modules/api') }, { name: 'authcopy', path: path.resolve(__dirname, './modules/authcopy') }, { name: 'basecopy', path: path.resolve(__dirname, './modules/basecopy') }, { name: 'graphqlcopy', path: path.resolve(__dirname, './modules/graphqlcopy') }, { name: '404page', path: path.resolve(__dirname, './modules/404page') }, { name: 'error', path: path.resolve(__dirname, './modules/error') }, { name: 'delegate', path: path.resolve(__dirname, './modules/delegate') }, { name: 'middleware', path: path.resolve(__dirname, './modules/handler') } ]; // Load routes and middleware functions modules.forEach((module) => { try { // Load middleware functions getModuleMiddlewares(module.path); // Load routes loadModuleRoutes(module.path); } catch (e) { error(e); process.exit(0); } }); // TODO: load extensions, themes const routes = getRoutes(); // Adding default middlewares addDefaultMiddlewareFuncs(app, routes); /** Hack for 'no route' case */ // routes.push({ // id: 'noRoute', // path: '/*', // method: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] // }); // routes.forEach((route) => { // app.all(route.path, Handler.middleware()); // }); app.use(Handler.middleware()); const bootstrap = async (server) => { server.listen(); await once(server, 'listening'); return server.address().port; }; const close = (server, done) => { server.close(done); }; export { app, bootstrap, close }; ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/404page/pages/frontStore/product/[loadProduct]loadCategory.js ================================================ import jest from 'jest-mock'; export default jest.fn((request, response) => {}); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/404page/pages/frontStore/product/[loadProduct]loadProductImage.js ================================================ import jest from 'jest-mock'; export default jest.fn(async (request, response, next) => { next(); }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/404page/pages/frontStore/product/loadProduct.js ================================================ import jest from 'jest-mock'; export default jest.fn(async (request, response, next) => { try { response.status(404); next(); } catch (e) { next(e); } }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/404page/pages/frontStore/product/route.json ================================================ { "methods": ["GET"], "path": "/product/:id", "name": "Product single page" } ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/api/api/createA/index.js ================================================ import jest from 'jest-mock'; export default jest.fn((request, response) => {}); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/api/api/createA/route.json ================================================ { "methods": ["POST"], "path": "/as" } ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/api/api/global/apiGlobal.js ================================================ import jest from 'jest-mock'; export default jest.fn((request, response) => {}); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/authcopy/api/createA/[index]afterIndex.js ================================================ import jest from 'jest-mock'; export default jest.fn((request, response) => {}); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/authcopy/api/global/[context]auth.js ================================================ export default (request, response) => { // Do nothing }; ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/authcopy/api/global/apiAuthGlobal.js ================================================ import jest from 'jest-mock'; export default jest.fn((request, response) => {}); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/authcopy/pages/global/[context]auth.js ================================================ export default (request, response) => {}; ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/basecopy/api/global/[apiResponse]apiErrorHandler.js ================================================ import apiErrorHandler from '../../../../../../../../modules/base/api/global/[apiResponse]apiErrorHandler.js'; export default apiErrorHandler; ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/basecopy/api/global/[auth]apiResponse[apiErrorHandler].js ================================================ import apiResponse from '../../../../../../../../modules/base/api/global/[auth]apiResponse[apiErrorHandler].js'; export default apiResponse; ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/basecopy/api/global/[auth]payloadValidate.js ================================================ import payloadValidate from '../../../../../../../../modules/base/api/global/[auth]payloadValidate.js'; export default payloadValidate; ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/basecopy/api/global/[payloadValidate]escapeHtml.js ================================================ import escapeHtml from '../../../../../../../../modules/base/api/global/[payloadValidate]escapeHtml.js'; export default escapeHtml; ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/basecopy/api/global/context.js ================================================ import { setContextValue } from '../../../../../../../../modules/graphql/services/contextHelper.js'; export default (request, response) => { response.context = {}; // TODO: Fix this setContextValue( request, 'homeUrl', `${request.protocol}://${request.get('host')}` ); setContextValue( request, 'currentUrl', `${request.protocol}://${request.get('host')}${request.originalUrl}` ); setContextValue(request, 'baseUrl', request.baseUrl); setContextValue(request, 'body', request.body); setContextValue(request, 'cookies', request.cookies); setContextValue(request, 'fresh', request.fresh); setContextValue(request, 'hostname', request.hostname); setContextValue(request, 'ip', request.ip); setContextValue(request, 'ips', request.ips); setContextValue(request, 'method', request.method); setContextValue(request, 'originalUrl', request.originalUrl); setContextValue(request, 'params', request.params); setContextValue(request, 'path', request.path); setContextValue(request, 'protocol', request.protocol); setContextValue(request, 'query', request.query); setContextValue(request, 'route', request.route); setContextValue(request, 'secure', request.secure); setContextValue(request, 'signedCookies', request.signedCookies); setContextValue(request, 'stale', request.stale); setContextValue(request, 'subdomains', request.subdomains); setContextValue(request, 'xhr', request.xhr); }; ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/basecopy/pages/admin/adminStaticAsset/route.json ================================================ { "methods": ["GET"], "path": "/assets/*" } ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/basecopy/pages/admin/adminStaticAsset/staticAssets.js ================================================ import staticMiddleware from '../../../../../../../../../modules/cms/pages/admin/adminStaticAsset/staticAssets.js'; export default staticMiddleware; ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/basecopy/pages/admin/all/adminTitle.js ================================================ export default (request, response, next) => { next(); }; ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/basecopy/pages/frontStore/all/title.js ================================================ export default (request, response, next) => { next(); }; ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/basecopy/pages/frontStore/notFound/route.json ================================================ { "methods": ["GET"], "path": "/notFound" } ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/basecopy/pages/frontStore/staticAsset/[context]staticAssets[auth].js ================================================ import staticMiddleware from '../../../../../../../../../modules/cms/pages/frontStore/staticAsset/[context]staticAssets[auth].js'; export default staticMiddleware; ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/basecopy/pages/frontStore/staticAsset/route.json ================================================ { "methods": ["GET"], "path": "/assets/*" } ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/basecopy/pages/global/[auth]notFound[response].js ================================================ import jest from 'jest-mock'; import notFound from '../../../../../../../../modules/base/pages/global/[auth]notFound[response].js'; export default jest.fn(notFound); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/basecopy/pages/global/[notFound]dummy[response].js ================================================ import jest from 'jest-mock'; export default jest.fn((request, response) => {}); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/basecopy/pages/global/[response]errorHandler.js ================================================ import jest from 'jest-mock'; import errorHandler from '../../../../../../../../modules/base/pages/global/[response]errorHandler.js'; export default jest.fn(errorHandler); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/basecopy/pages/global/context.js ================================================ import { setContextValue } from '../../../../../../../../modules/graphql/services/contextHelper.js'; export default (request, response) => { response.context = {}; // TODO: Fix this setContextValue( request, 'homeUrl', `${request.protocol}://${request.get('host')}` ); setContextValue( request, 'currentUrl', `${request.protocol}://${request.get('host')}${request.originalUrl}` ); setContextValue(request, 'baseUrl', request.baseUrl); setContextValue(request, 'body', request.body); setContextValue(request, 'cookies', request.cookies); setContextValue(request, 'fresh', request.fresh); setContextValue(request, 'hostname', request.hostname); setContextValue(request, 'ip', request.ip); setContextValue(request, 'ips', request.ips); setContextValue(request, 'method', request.method); setContextValue(request, 'originalUrl', request.originalUrl); setContextValue(request, 'params', request.params); setContextValue(request, 'path', request.path); setContextValue(request, 'protocol', request.protocol); setContextValue(request, 'query', request.query); setContextValue(request, 'route', request.route); setContextValue(request, 'secure', request.secure); setContextValue(request, 'signedCookies', request.signedCookies); setContextValue(request, 'stale', request.stale); setContextValue(request, 'subdomains', request.subdomains); setContextValue(request, 'xhr', request.xhr); }; ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/basecopy/pages/global/response[errorHandler].js ================================================ import jest from 'jest-mock'; import response from '../../../../../../../../modules/base/pages/global/response[errorHandler].js'; export default jest.fn(response); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/delegate/pages/frontStore/delegateTest/asyncWithNext[collection].js ================================================ import axios from 'axios'; export default async (request, response, next) => { const content = await axios.get( 'https://jsonplaceholder.typicode.com/todos/1' ); next(); }; ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/delegate/pages/frontStore/delegateTest/async[collection].js ================================================ import axios from 'axios'; export default async (request, response) => { const content = await axios.get( 'https://jsonplaceholder.typicode.com/todos/1' ); }; ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/delegate/pages/frontStore/delegateTest/collection.js ================================================ import { getDelegates, setDelegate } from '../../../../../../../delegate.js'; let delegates; function collection(request, response, next) { delegates = getDelegates(request); return response.status(200).json({ ok: true }); } export default collection; export { delegates }; ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/delegate/pages/frontStore/delegateTest/returnOne[returnTwo].js ================================================ import jest from 'jest-mock'; export default jest.fn((request, response) => 1); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/delegate/pages/frontStore/delegateTest/returnThree[collection].js ================================================ import jest from 'jest-mock'; export default jest.fn((request, response) => 3); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/delegate/pages/frontStore/delegateTest/returnTwo[returnThree].js ================================================ import jest from 'jest-mock'; export default jest.fn((request, response, next) => { next(); }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/delegate/pages/frontStore/delegateTest/route.json ================================================ { "methods": ["GET"], "path": "/delegateTest" } ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/delegate/pages/frontStore/delegateTest/syncOne.js ================================================ ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/delegate/pages/frontStore/delegateTest/syncWithNext[collection].js ================================================ import jest from 'jest-mock'; export default jest.fn((request, response, next) => { next(); }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/delegate/pages/frontStore/delegateTest/sync[collection].js ================================================ import jest from 'jest-mock'; export default jest.fn((request, response) => { const a = 1; }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/error/pages/frontStore/errorHandlerTest/errorInAsync.js ================================================ export default async (request, response) => { const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); await delay(3000); undefined.b = 1; }; ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/error/pages/frontStore/errorHandlerTest/errorInAsyncWithNext.js ================================================ import jest from 'jest-mock'; export default jest.fn(async (request, response, next) => { try { const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); await delay(3000); undefined.a = 1; next(); } catch (e) { next(e); } }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/error/pages/frontStore/errorHandlerTest/errorInSync.js ================================================ import jest from 'jest-mock'; export default jest.fn((request, response) => { throw new Error('Error in sync'); }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/error/pages/frontStore/errorHandlerTest/errorInSyncWithNext.js ================================================ import jest from 'jest-mock'; export default jest.fn((request, response, next) => { next(new Error('Error in sync with next')); }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/error/pages/frontStore/errorHandlerTest/route.json ================================================ { "methods": ["GET"], "path": "/errorHandlerTest" } ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/graphqlcopy/pages/global/[bodyParser]buildQuery[graphql].js ================================================ export default (request, response) => {}; ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/graphqlcopy/pages/global/[buildQuery]graphql[notFound].js ================================================ export default (request, response) => {}; ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/graphqlcopy/pages/global/bodyParser[buildQuery].js ================================================ import bodyParser from 'body-parser'; export default (request, response, next) => { bodyParser.json({ inflate: false })(request, response, () => { bodyParser.urlencoded({ extended: true })(request, response, next); }); }; ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/handler/pages/admin/productEdit/[loadProduct]loadCategory.js ================================================ import jest from 'jest-mock'; export default jest.fn((request, response) => {}); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/handler/pages/admin/productEdit/[loadProduct]loadProductImage.js ================================================ import jest from 'jest-mock'; export default jest.fn(async (request, response, next) => { next(); }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/handler/pages/admin/productEdit/loadProduct.js ================================================ import jest from 'jest-mock'; export default jest.fn(async (request, response, next) => { try { response.status(404); next(); } catch (e) { next(e); } }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/handler/pages/admin/productEdit/route.json ================================================ { "methods": ["GET"], "path": "/product/edit/:id" } ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/handler/pages/frontStore/middleware/[loadAttribute]loadOptions.js ================================================ import jest from 'jest-mock'; export default jest.fn((request, response) => {}); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/handler/pages/frontStore/middleware/[loadProductImage]loadAttribute.js ================================================ import jest from 'jest-mock'; export default jest.fn((request, response) => { throw new Error('this is an error'); }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/handler/pages/frontStore/middleware/[loadProduct]loadCategory.js ================================================ import jest from 'jest-mock'; export default jest.fn((request, response) => {}); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/handler/pages/frontStore/middleware/[loadProduct]loadProductImage.js ================================================ import jest from 'jest-mock'; export default jest.fn(async (request, response, next) => { next(); }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/handler/pages/frontStore/middleware/[syncOne,asyncOne]checkExecutionOrderAsync[loadAttribute].js ================================================ import jest from 'jest-mock'; export default jest.fn(async (request, response) => { if (!request.syncOneCompleted) { throw new Error('syncOne middleware should be completed first'); } if (!request.asyncOneCompleted) { throw new Error('asyncOne middleware should be completed first'); } }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/handler/pages/frontStore/middleware/[syncOne,asyncOne]checkExecutionOrder[loadAttribute].js ================================================ import jest from 'jest-mock'; export default jest.fn((request, response) => { if (!request.syncOneCompleted) { throw new Error('syncOne middleware should be completed first'); } if (!request.asyncOneCompleted) { throw new Error('asyncOne middleware should be completed first'); } }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/handler/pages/frontStore/middleware/asyncOne[loadAttribute].js ================================================ import jest from 'jest-mock'; export default jest.fn(async (request, response) => { await new Promise((r) => setTimeout(r, 200)); request.asyncOneCompleted = true; }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/handler/pages/frontStore/middleware/loadProduct[loadAttribute].js ================================================ import jest from 'jest-mock'; export default jest.fn(async (request, response, next) => { next(); }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/handler/pages/frontStore/middleware/route.json ================================================ { "methods": ["GET"], "path": "/middleware" } ================================================ FILE: packages/evershop/src/lib/middleware/tests/app/modules/handler/pages/frontStore/middleware/syncOne[loadAttribute].js ================================================ import jest from 'jest-mock'; export default jest.fn((request, response) => { request.syncOneCompleted = true; }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/unit/404page.handling.test.js ================================================ import http from 'http'; import axios from 'axios'; import { app, bootstrap, close } from '../app/app.js'; import notFound from '../app/modules/basecopy/pages/global/[auth]notFound[response].js'; import dummy from '../app/modules/basecopy/pages/global/[notFound]dummy[response].js'; import response from '../app/modules/basecopy/pages/global/response[errorHandler].js'; import loadProductImage from '../app/modules/404page/pages/frontStore/product/[loadProduct]loadProductImage.js'; import loadCategory from '../app/modules/404page/pages/frontStore/product/[loadProduct]loadCategory.js'; import loadProduct from '../app/modules/404page/pages/frontStore/product/loadProduct.js'; import { jest, describe, it, expect, beforeAll, afterAll } from '@jest/globals'; jest.setTimeout(80000); describe('buildMiddlewareFunction', () => { const server = http.createServer(app); let port; beforeAll(async () => { port = await bootstrap(server); }); it('It should return 404 page when route is not exist', async () => { // Visit a url const response = await axios.get( `http://localhost:${port}/noexistedroute`, { validateStatus(status) { return status >= 200 && status < 600; } } ); expect(response.status).toEqual(404); expect(notFound).toHaveBeenCalledTimes(1); }); it('It should return 404 page when middleware sets status to 404', async () => { // Visit a url const response = await axios.get(`http://localhost:${port}/product/404`, { validateStatus(status) { return status >= 200 && status < 500; } }); expect(response.status).toEqual(404); }); it('It should bypass the rouded middleware when status is 404', async () => { expect(loadProductImage).toHaveBeenCalledTimes(0); expect(loadCategory).toHaveBeenCalledTimes(0); expect(loadProduct).toHaveBeenCalledTimes(1); }); it('It should not bypass the app level middleware when status is 404', async () => { expect(notFound).toHaveBeenCalledTimes(2); expect(dummy).toHaveBeenCalledTimes(2); expect(response).toHaveBeenCalledTimes(2); }); afterAll((done) => { close(server, done); }); }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/unit/500error.handling.test.js ================================================ import { app, bootstrap, close } from '../app/app.js'; import axios from 'axios'; import http from 'http'; import errorHandler from '../app/modules/basecopy/pages/global/[response]errorHandler.js'; import { jest, describe, it, expect, beforeAll, afterAll } from '@jest/globals'; jest.setTimeout(800000); describe('buildMiddlewareFunction', () => { const server = http.createServer(app); let port; beforeAll(async () => { port = await bootstrap(server); }); it('It should return 500 error when a error occurred', async () => { // Visit a url const response = await axios.get( `http://localhost:${port}/errorHandlerTest`, { validateStatus(status) { return status >= 200 && status < 600; } } ); expect(response.status).toEqual(500); expect(response.data.split(/\r\n|\r|\n/).length).toEqual(1); }); it('The error handler middleware should be executed only one time per request', async () => { expect(errorHandler).toHaveBeenCalledTimes(1); }); afterAll((done) => { close(server, done); }); }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/unit/apiHandler.middleware.test.js ================================================ import http from 'http'; import axios from 'axios'; import { app, bootstrap, close } from '../app/app.js'; import createA from '../app/modules/api/api/createA/index.js'; import afterIndex from '../app/modules/authcopy/api/createA/[index]afterIndex.js'; import createAGlobal from '../app/modules/api/api/global/apiGlobal.js'; import authApiGlobal from '../app/modules/authcopy/api/global/apiAuthGlobal.js'; import { jest, describe, it, expect, beforeAll, afterAll } from '@jest/globals'; jest.setTimeout(80000); describe('test API middleware', () => { const server = http.createServer(app); let port; beforeAll(async () => { port = await bootstrap(server); }); it('It should execute the valid middleware functions', async () => { try { await axios.post(`http://localhost:${port}/api/as`, { validateStatus(status) { return status >= 200 && status < 600; } }); } catch (e) { console.log(e.response.data); } expect(createA).toHaveBeenCalledTimes(1); expect(afterIndex).toHaveBeenCalledTimes(1); expect(createAGlobal).toHaveBeenCalledTimes(1); expect(authApiGlobal).toHaveBeenCalledTimes(1); }); afterAll((done) => { close(server, done); }); }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/unit/delegate.test.js ================================================ import http from 'http'; import axios from 'axios'; import { app, bootstrap, close } from '../app/app.js'; import { jest, describe, it, expect, beforeAll, afterAll } from '@jest/globals'; import { delegates } from '../app/modules/delegate/pages/frontStore/delegateTest/collection.js'; import { getDelegateManager } from '../../delegate.js'; jest.setTimeout(80000); describe('test delegate', () => { const server = http.createServer(app); let port; beforeAll(async () => { port = await bootstrap(server); }); it('It should allow set delegate the fist time', () => { const delegate = getDelegateManager({ locals: {} }); delegate.setOnce('returnOne', 1); expect(delegate.get('returnOne')).toEqual(1); }); it('It should throw error when set delegate again', () => { const delegate = getDelegateManager({ locals: {} }); delegate.setOnce('returnOne', 1); expect(() => { delegate.setOnce('returnOne', 2); }).toThrowError('is already set'); }); it('It should allow to get all values', () => { const delegate = getDelegateManager({ locals: {} }); delegate.setOnce('returnTwo', 2); delegate.setOnce('returnThree', 3); expect(delegate.getAll()).toEqual({ returnTwo: 2, returnThree: 3 }); expect(delegate.keys()).toEqual(['returnTwo', 'returnThree']); }); it('It should give a cloned value instead of the original value', () => { const delegate = getDelegateManager({ locals: {} }); delegate.setOnce('returnOne', 1); delegate.setOnce('returnTwo', { value: 2 }); let value = delegate.get('returnOne'); value += 1; // modify the value const objectValue = delegate.get('returnTwo'); objectValue.value += 1; // modify the value expect(value).toEqual(2); // cloned value should not change expect(objectValue.value).toEqual(3); expect(delegate.get('returnOne')).toEqual(1); // original value should not change }); it('Middleware function return desired value', async () => { // Visit a url await axios.get(`http://localhost:${port}/delegateTest`, { validateStatus(status) { return status >= 200 && status < 600; } }); expect(delegates.returnOne).toEqual(1); expect(delegates.returnTwo).toEqual(undefined); expect(delegates.returnThree).toEqual(3); }); afterAll((done) => { close(server, done); }); }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/unit/handler.getMiddlewaresByRoute.test.js ================================================ import { Handler } from '../../Handler.js'; import '../app/app.js'; import { jest, describe, it, expect, beforeAll, afterAll } from '@jest/globals'; describe('test getMiddlewaresByRoute', () => { const productMiddleweres = Handler.getMiddlewareByRoute({ id: 'product', isAdmin: false }); it('should contains middlewares for product route', () => { expect( productMiddleweres.findIndex((m) => m.id === 'loadCategory') ).not.toBe(-1); expect( productMiddleweres.findIndex((m) => m.id === 'loadProductImage') ).not.toBe(-1); }); it('should contains frontStore level middleware', () => { expect(productMiddleweres.findIndex((m) => m.id === 'title')).not.toBe(-1); }); it('should contains application level middleware', () => { expect(productMiddleweres.findIndex((m) => m.id === 'auth')).not.toBe(-1); expect( productMiddleweres.findIndex((m) => m.id === 'errorHandler') ).not.toBe(-1); expect(productMiddleweres.findIndex((m) => m.id === 'context')).not.toBe( -1 ); expect(productMiddleweres.findIndex((m) => m.id === 'response')).not.toBe( -1 ); }); const productEditMiddleweres = Handler.getMiddlewareByRoute({ id: 'productEdit', isAdmin: true }); it('should contains middlewares for product route', () => { expect( productEditMiddleweres.findIndex((m) => m.id === 'loadCategory') ).not.toBe(-1); expect( productEditMiddleweres.findIndex((m) => m.id === 'loadProductImage') ).not.toBe(-1); }); it('should contains admin level middleware', () => { expect( productEditMiddleweres.findIndex((m) => m.id === 'adminTitle') ).not.toBe(-1); }); it('should contains application level middleware', () => { expect(productEditMiddleweres.findIndex((m) => m.id === 'auth')).not.toBe( -1 ); expect( productEditMiddleweres.findIndex((m) => m.id === 'errorHandler') ).not.toBe(-1); expect( productEditMiddleweres.findIndex((m) => m.id === 'context') ).not.toBe(-1); expect( productEditMiddleweres.findIndex((m) => m.id === 'response') ).not.toBe(-1); }); }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/unit/handlers.middleware.test.js ================================================ import { app, bootstrap, close } from '../app/app.js'; import axios from 'axios'; import http from 'http'; import loadProductAttribute from '../app/modules/handler/pages/frontStore/middleware/[loadProductImage]loadAttribute.js'; import loadProductImage from '../app/modules/handler/pages/frontStore/middleware/[loadProduct]loadProductImage.js'; import loadCategory from '../app/modules/handler/pages/frontStore/middleware/[loadProduct]loadCategory.js'; import loadProduct from '../app/modules/handler/pages/frontStore/middleware/loadProduct[loadAttribute].js'; import loadProductOption from '../app/modules/handler/pages/frontStore/middleware/[loadAttribute]loadOptions.js'; import syncOne from '../app/modules/handler/pages/frontStore/middleware/syncOne[loadAttribute].js'; import asyncOne from '../app/modules/handler/pages/frontStore/middleware/asyncOne[loadAttribute].js'; import checkExecutionOrder from '../app/modules/handler/pages/frontStore/middleware/[syncOne,asyncOne]checkExecutionOrder[loadAttribute].js'; import checkExecutionOrderAsync from '../app/modules/handler/pages/frontStore/middleware/[syncOne,asyncOne]checkExecutionOrderAsync[loadAttribute].js'; import { jest, describe, it, expect, beforeAll, afterAll } from '@jest/globals'; jest.setTimeout(80000); describe('test middleware', () => { const server = http.createServer(app); let port; beforeAll(async () => { port = await bootstrap(server); }); it('It should only execute the next middleware function when the previous one is completed', async () => { // Visit a url const response = await axios.get(`http://localhost:${port}/middleware`, { validateStatus(status) { return status >= 200 && status <= 500; } }); expect(syncOne).toHaveBeenCalledTimes(1); expect(asyncOne).toHaveBeenCalledTimes(1); expect(checkExecutionOrder).toHaveBeenCalledTimes(1); expect(checkExecutionOrderAsync).toHaveBeenCalledTimes(1); expect(loadProductAttribute).toHaveBeenCalledTimes(1); }); it('It should execute the good middleware functions', async () => { const response = await axios.get(`http://localhost:${port}/middleware`, { validateStatus(status) { return status >= 200 && status <= 500; } }); expect(loadProductAttribute).toHaveBeenCalledTimes(2); expect(loadProductImage).toHaveBeenCalledTimes(2); expect(loadCategory).toHaveBeenCalledTimes(2); expect(loadProduct).toHaveBeenCalledTimes(2); }); it('It should not execute the middleware functions after error occurred', async () => { expect(loadProductOption).toHaveBeenCalledTimes(0); }); afterAll((done) => { close(server, done); }); }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/unit/middleware.buildMiddlewareFunction.test.js ================================================ import { buildMiddlewareFunction } from '../../buildMiddlewareFunction.js'; import path from 'path'; import { jest, describe, it, expect } from '@jest/globals'; expect.extend({ nullOrAny(received, expected) { if (received === null) { return { pass: true, message: () => `expected null or instance of ${this.utils.printExpected( expected )}, but received ${this.utils.printReceived(received)}` }; } if (expected == String) { return { pass: typeof received === 'string' || received instanceof String, message: () => `expected null or instance of ${this.utils.printExpected( expected )}, but received ${this.utils.printReceived(received)}` }; } if (expected == Number) { return { pass: typeof received === 'number' || received instanceof Number, message: () => `expected null or instance of ${this.utils.printExpected( expected )}, but received ${this.utils.printReceived(received)}` }; } if (expected == Function) { return { pass: typeof received === 'function' || received instanceof Function, message: () => `expected null or instance of ${this.utils.printExpected( expected )}, but received ${this.utils.printReceived(received)}` }; } if (expected == Object) { return { pass: received !== null && typeof received === 'object', message: () => `expected null or instance of ${this.utils.printExpected( expected )}, but received ${this.utils.printReceived(received)}` }; } if (expected == Boolean) { return { pass: typeof received === 'boolean', message: () => `expected null or instance of ${this.utils.printExpected( expected )}, but received ${this.utils.printReceived(received)}` }; } /* jshint -W122 */ /* global Symbol */ if (typeof Symbol !== 'undefined' && this.expectedObject == Symbol) { return { pass: typeof received === 'symbol', message: () => `expected null or instance of ${this.utils.printExpected( expected )}, but received ${this.utils.printReceived(received)}` }; } /* jshint +W122 */ return { pass: received instanceof expected, message: () => `expected null or instance of ${this.utils.printExpected( expected )}, but received ${this.utils.printReceived(received)}` }; } }); describe('buildMiddlewareFunction', () => { it('It should thrown an exception if id is not valid', () => { expect(() => buildMiddlewareFunction('a b', '/catalog/controllers/product.js') ).toThrow(Error); }); it('It should return a function if id is valid', () => { const middleware = buildMiddlewareFunction( 'abc', '/catalog/controllers/product.js' ); expect(typeof middleware).toBe('function'); }); }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/unit/middleware.getRouteFromPath.test.js ================================================ import { resolve } from 'path'; import { getRouteFromPath } from '../../getRouteFromPath.js'; describe('Test getRouteFromPath function', () => { it('Parse app level route', () => { expect(getRouteFromPath(resolve('/catalog/pages/global/title.js'))).toEqual( { region: 'pages', scope: 'app', routeId: null } ); expect(getRouteFromPath(resolve('/cms/api/global/title.js'))).toEqual({ region: 'api', scope: 'app', routeId: null }); }); it('Parse admin level route', () => { expect( getRouteFromPath(resolve('/catalog/pages/admin/all/title.js')) ).toEqual({ region: 'pages', scope: 'admin', routeId: 'admin' }); expect(getRouteFromPath(resolve('/cms/api/admin/all/title.js'))).toEqual({ region: 'api', scope: 'admin', routeId: 'admin' }); }); it('Parse frontStore level route', () => { expect( getRouteFromPath(resolve('/catalog/pages/frontStore/all/title.js')) ).toEqual({ region: 'pages', scope: 'frontStore', routeId: 'frontStore' }); expect( getRouteFromPath(resolve('/cms/api/frontStore/all/title.js')) ).toEqual({ region: 'api', scope: 'frontStore', routeId: 'frontStore' }); }); it('Parse admin routed level route', () => { expect( getRouteFromPath(resolve('/catalog/pages/admin/product/title.js')) ).toEqual({ region: 'pages', scope: 'admin', routeId: 'product' }); expect( getRouteFromPath(resolve('/cms/api/admin/product/title.js')) ).toEqual({ region: 'api', scope: 'admin', routeId: 'product' }); }); it('Parse frontStore routed level route', () => { expect( getRouteFromPath(resolve('/catalog/pages/frontStore/product/title.js')) ).toEqual({ region: 'pages', scope: 'frontStore', routeId: 'product' }); expect( getRouteFromPath(resolve('/cms/api/frontStore/product/title.js')) ).toEqual({ region: 'api', scope: 'frontStore', routeId: 'product' }); }); it('Parse multi admin routed level route', () => { expect( getRouteFromPath( resolve('/catalog/pages/admin/product+category/title.js') ) ).toEqual({ region: 'pages', scope: 'admin', routeId: ['product', 'category'] }); expect( getRouteFromPath(resolve('/cms/api/admin/product+category/title.js')) ).toEqual({ region: 'api', scope: 'admin', routeId: ['product', 'category'] }); }); it('Parse multi frontStore routed level route', () => { expect( getRouteFromPath( resolve('/catalog/pages/frontStore/product+category/title.js') ) ).toEqual({ region: 'pages', scope: 'frontStore', routeId: ['product', 'category'] }); expect( getRouteFromPath(resolve('/cms/api/frontStore/product+category/title.js')) ).toEqual({ region: 'api', scope: 'frontStore', routeId: ['product', 'category'] }); }); it('Parse invalid path', () => { expect(() => getRouteFromPath( resolve('/catalog/controllers/fro ntStore/product/title.js') ) ).toThrow(); expect(() => getRouteFromPath( resolve('/catalog/controllers/frontStore/pro2uct/title.js') ) ).toThrow(); }); }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/unit/middleware.noDublicateId.test.js ================================================ import { noDublicateId } from '../../noDuplicateId.js'; describe('Test noDublicateId function', () => { it('It should return false if routed middlewareID is existed', () => { expect( noDublicateId( [ { id: 'routeOne', routeId: 'home' } ], { id: 'routeOne', routeId: 'home' } ) ).toEqual(false); }); it('It should return false if admin level middlewareID is existed', () => { expect( noDublicateId( [ { id: 'routeOne', routeId: 'admin' } ], { id: 'routeOne', routeId: 'admin' } ) ).toEqual(false); }); it('It should return false if admin level middlewareID is existed', () => { expect( noDublicateId( [ { id: 'routeOne', routeId: 'admin', scope: 'admin' } ], { id: 'routeOne', routeId: 'admin' }, 'admin' ) ).toEqual(false); }); it('It should return false if frontStore level middlewareID is existed', () => { expect( noDublicateId( [ { id: 'routeOne', routeId: 'frontStore' } ], { id: 'routeOne', routeId: 'frontStore' } ) ).toEqual(false); }); it('It should return false if application level middlewareID is existed', () => { expect( noDublicateId( [ { id: 'routeOne', routeId: null } ], { id: 'routeOne', routeId: null } ) ).toEqual(false); }); it('It should return false if routeId is null', () => { expect( noDublicateId( [ { id: 'routeOne', routeId: null } ], { id: 'routeOne', routeId: 'home' } ) ).toEqual(false); }); it('It should return false if routeId is admin', () => { expect( noDublicateId( [ { id: 'routeOne', routeId: 'admin' } ], { id: 'routeOne', routeId: 'home' } ) ).toEqual(false); }); it('It should return false if routeId is frontStore', () => { expect( noDublicateId( [ { id: 'routeOne', routeId: 'frontStore' } ], { id: 'routeOne', routeId: 'home' } ) ).toEqual(false); }); it('It should return true if routeId is different', () => { expect( noDublicateId( [ { id: 'routeOne', routeId: 'home' } ], { id: 'routeOne', routeId: 'category' } ) ).toEqual(true); }); }); ================================================ FILE: packages/evershop/src/lib/middleware/tests/unit/middleware.scanForMiddlewareFunctions.test.js ================================================ import { noDublicateId } from '../../noDuplicateId.js'; describe('Test scanForMiddlewareFunctions function', () => { it('It should return false if routed middlewareID is existed', () => { expect( noDublicateId( [ { id: 'routeOne', routeId: 'home' } ], { id: 'routeOne', routeId: 'home' } ) ).toEqual(false); }); it('It should return false if admin middlewareID is existed', () => { expect( noDublicateId( [ { id: 'routeOne', routeId: 'admin' } ], { id: 'routeOne', routeId: 'admin' } ) ).toEqual(false); }); it('It should return false if frontStore middlewareID is existed', () => { expect( noDublicateId( [ { id: 'routeOne', routeId: null } ], { id: 'routeOne', routeId: null } ) ).toEqual(false); }); it('It should return false if routeId is null', () => { expect( noDublicateId( [ { id: 'routeOne', routeId: null } ], { id: 'routeOne', routeId: 'home' } ) ).toEqual(false); }); it('It should return false if routeId is admin', () => { expect( noDublicateId( [ { id: 'routeOne', routeId: 'admin' } ], { id: 'routeOne', routeId: 'home' } ) ).toEqual(false); }); it('It should return false if routeId is frontStore', () => { expect( noDublicateId( [ { id: 'routeOne', routeId: 'frontStore' } ], { id: 'routeOne', routeId: 'home' } ) ).toEqual(false); }); it('It should return true if routeId is different', () => { expect( noDublicateId( [ { id: 'routeOne', routeId: 'home' } ], { id: 'routeOne', routeId: 'category' } ) ).toEqual(true); }); }); ================================================ FILE: packages/evershop/src/lib/middlewares/bodyJson.ts ================================================ import bodyParser from 'body-parser'; import { EvershopRequest } from '../../types/request.js'; import { EvershopResponse } from '../../types/response.js'; export default (request: EvershopRequest, response: EvershopResponse, next) => { bodyParser.json()(request, response, next); }; ================================================ FILE: packages/evershop/src/lib/middlewares/multerNone.ts ================================================ import multer from 'multer'; const upload = multer(); export default (request, response, next) => { upload.none()(request, response, next); }; ================================================ FILE: packages/evershop/src/lib/middlewares/publicStatic.ts ================================================ import fs from 'fs/promises'; import { join } from 'path'; import staticMiddleware from 'serve-static'; import { EvershopRequest } from '../../types/request.js'; import { EvershopResponse } from '../../types/response.js'; import { CONSTANTS } from '../helpers.js'; export default async function publicStatic( request: EvershopRequest, response: EvershopResponse, next ) { // Get the request path const { path } = request; try { if (!path.includes('.')) { throw new Error('No file extension'); } // Asynchoronously check if the path is a file and exists in the public folder const test = await fs.stat(join(CONSTANTS.ROOTPATH, 'public', path)); if (test.isFile()) { // If it is a file, serve it staticMiddleware(join(CONSTANTS.ROOTPATH, 'public'))( request, response, next ); } } catch (e) { // If the path is not a file or does not exist in the public folder, call next next(); } } ================================================ FILE: packages/evershop/src/lib/middlewares/static.ts ================================================ import { constants } from 'fs'; import { access, stat } from 'fs/promises'; import { join, normalize, extname } from 'path'; import staticMiddleware from 'serve-static'; import { EvershopRequest } from '../..//types/request.js'; import { EvershopResponse } from '../../types/response.js'; import { CONSTANTS } from '../helpers.js'; // Define allowed file extensions (whitelist) const ALLOWED_EXTENSIONS = [ // Images '.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', '.avif', '.ico', // Styles '.css', '.scss', '.less', // Scripts '.js', '.jsx', '.mjs', '.ts', '.tsx', // Fonts '.woff', '.woff2', '.eot', '.ttf', '.otf', // Documents '.pdf', // Data '.json', '.map' ]; /** * Checks if a path exists and is accessible * @param {string} path - Path to check * @returns {Promise} True if the path exists and is accessible */ const pathExists = async (path: string): Promise => { try { await access(path, constants.F_OK); return true; } catch { return false; } }; /** * Validates if the path is a valid file and its extension is allowed * @param {string} fullPath - Full path to the file * @returns {Promise} True if the path is valid and allowed */ const isValidFile = async (fullPath: string): Promise => { try { // Check if file exists and is a file (not a directory) const stats = await stat(fullPath); if (!stats.isFile()) { return false; } // Check if the file extension is in the allowed list const ext = extname(fullPath).toLowerCase(); return ALLOWED_EXTENSIONS.includes(ext); } catch (error) { // File doesn't exist or other error return false; } }; const staticMiddlewareOptions = { maxAge: '1y', immutable: true, setHeaders: (res) => { res.setHeader('X-Content-Type-Options', 'nosniff'); } }; export default async ( request: EvershopRequest, response: EvershopResponse, next ) => { let path; if (request.isAdmin === true) { path = normalize(request.originalUrl.replace('/admin/assets/', '')); } else { path = normalize(request.originalUrl.replace('/assets/', '')); } // Prevent path traversal attacks if (path.includes('..')) { return response.status(403).send('Forbidden'); } if (request.isAdmin === true) { request.originalUrl = request.originalUrl.replace('/admin/assets', ''); request.url = request.originalUrl.replace('/admin/assets', ''); } else { request.originalUrl = request.originalUrl.replace('/assets', ''); request.url = request.originalUrl.replace('/assets', ''); } if (path.endsWith('/')) { return response.status(404).send('Not Found'); } // Check build path const buildPath = join(CONSTANTS.ROOTPATH, '.evershop/build', path); if ((await pathExists(buildPath)) && (await isValidFile(buildPath))) { return staticMiddleware( join(CONSTANTS.ROOTPATH, '.evershop/build'), staticMiddlewareOptions )(request, response, next); } // Check media path const mediaPath = join(CONSTANTS.MEDIAPATH, path); if ((await pathExists(mediaPath)) && (await isValidFile(mediaPath))) { return staticMiddleware(CONSTANTS.MEDIAPATH, staticMiddlewareOptions)( request, response, next ); } // Check public path const publicPath = join(CONSTANTS.ROOTPATH, 'public', path); if ((await pathExists(publicPath)) && (await isValidFile(publicPath))) { return staticMiddleware( join(CONSTANTS.ROOTPATH, 'public'), staticMiddlewareOptions )(request, response, next); } // If none of the above conditions are met return response.status(404).send('Not Found'); }; ================================================ FILE: packages/evershop/src/lib/middlewares/themePublicStatic.ts ================================================ import fs from 'fs/promises'; import { join } from 'path'; import staticMiddleware from 'serve-static'; import { EvershopRequest } from '../../types/request.js'; import { EvershopResponse } from '../../types/response.js'; import { getEnabledTheme } from '../util/getEnabledTheme.js'; export default async function themePublicStatic( request: EvershopRequest, response: EvershopResponse, next ) { // Get the request path const { path } = request; const theme = getEnabledTheme(); if (!theme) { next(); } else { try { if (!path.includes('.')) { throw new Error('No file extension'); } // Asynchoronously check if the path is a file and exists in the public folder const test = await fs.stat(join(theme.path, 'public', path)); if (test.isFile()) { // If it is a file, serve it staticMiddleware(join(theme.path, 'public'))(request, response, next); } } catch (e) { // If the path is not a file or does not exist in the public folder, call next next(); } } } ================================================ FILE: packages/evershop/src/lib/pathToRegexp.js ================================================ const isarray = (e) => Array.isArray(e); function parse(e, t) { for ( var r, n = [], o = 0, a = 0, i = '', p = (t && t.delimiter) || '/'; (r = PATH_REGEXP.exec(e)) != null; ) { const s = r[0]; const c = r[1]; const u = r.index; if (((i += e.slice(a, u)), (a = u + s.length), c)) i += c[1]; else { const l = e[a]; const g = r[2]; const f = r[3]; const x = r[4]; const h = r[5]; const d = r[6]; const m = r[7]; i && (n.push(i), (i = '')); const y = g != null && l != null && l !== g; const R = d === '+' || d === '*'; const T = d === '?' || d === '*'; const E = r[2] || p; const v = x || h; n.push({ name: f || o++, prefix: g || '', delimiter: E, optional: T, repeat: R, partial: y, asterisk: !!m, pattern: v ? escapeGroup(v) : m ? '.*' : `[^${escapeString(E)}]+?` }); } } return a < e.length && (i += e.substr(a)), i && n.push(i), n; } function compile(e, t) { return tokensToFunction(parse(e, t)); } function encodeURIComponentPretty(e) { return encodeURI(e).replace( /[\/?#]/g, (e) => `%${e.charCodeAt(0).toString(16).toUpperCase()}` ); } function encodeAsterisk(e) { return encodeURI(e).replace( /[?#]/g, (e) => `%${e.charCodeAt(0).toString(16).toUpperCase()}` ); } function tokensToFunction(e) { for (var t = new Array(e.length), r = 0; r < e.length; r++) typeof e[r] === 'object' && (t[r] = new RegExp(`^(?:${e[r].pattern})$`)); return function (r, n) { for ( var o = '', a = r || {}, i = n || {}, p = i.pretty ? encodeURIComponentPretty : encodeURIComponent, s = 0; s < e.length; s++ ) { const c = e[s]; if (typeof c !== 'string') { var u; const l = a[c.name]; if (l == null) { if (c.optional) { c.partial && (o += c.prefix); continue; } throw new TypeError(`Expected "${c.name}" to be defined`); } if (isarray(l)) { if (!c.repeat) throw new TypeError( `Expected "${ c.name }" to not repeat, but received \`${JSON.stringify(l)}\`` ); if (l.length === 0) { if (c.optional) continue; throw new TypeError(`Expected "${c.name}" to not be empty`); } for (let g = 0; g < l.length; g++) { if (((u = p(l[g])), !t[s].test(u))) throw new TypeError( `Expected all "${c.name}" to match "${ c.pattern }", but received \`${JSON.stringify(u)}\`` ); o += (g === 0 ? c.prefix : c.delimiter) + u; } } else { if (((u = c.asterisk ? encodeAsterisk(l) : p(l)), !t[s].test(u))) throw new TypeError( `Expected "${c.name}" to match "${c.pattern}", but received "${u}"` ); o += c.prefix + u; } } else o += c; } return o; }; } function escapeString(e) { return e.replace(/([.+*?=^!:${}()[\]|\/\\])/g, '\\$1'); } function escapeGroup(e) { return e.replace(/([=!:$\/()])/g, '\\$1'); } function attachKeys(e, t) { return (e.keys = t), e; } function flags(e) { return e.sensitive ? '' : 'i'; } function regexpToRegexp(e, t) { const r = e.source.match(/\((?!\?)/g); if (r) { for (let n = 0; n < r.length; n++) { t.push({ name: n, prefix: null, delimiter: null, optional: !1, repeat: !1, partial: !1, asterisk: !1, pattern: null }); } } return attachKeys(e, t); } function arrayToRegexp(e, t, r) { for (var n = [], o = 0; o < e.length; o++) n.push(pathToRegexp(e[o], t, r).source); return attachKeys(new RegExp(`(?:${n.join('|')})`, flags(r)), t); } function stringToRegexp(e, t, r) { return tokensToRegExp(parse(e, r), t, r); } function tokensToRegExp(e, t, r) { isarray(t) || ((r = t || r), (t = [])), (r = r || {}); for (var n = r.strict, o = !1 !== r.end, a = '', i = 0; i < e.length; i++) { const p = e[i]; if (typeof p === 'string') a += escapeString(p); else { const s = escapeString(p.prefix); let c = `(?:${p.pattern})`; t.push(p), p.repeat && (c += `(?:${s}${c})*`), (c = p.optional ? p.partial ? `${s}(${c})?` : `(?:${s}(${c}))?` : `${s}(${c})`), (a += c); } } const u = escapeString(r.delimiter || '/'); const l = a.slice(-u.length) === u; return ( n || (a = `${l ? a.slice(0, -u.length) : a}(?:${u}(?=$))?`), (a += o ? '$' : n && l ? '' : `(?=${u}|$)`), attachKeys(new RegExp(`^${a}`, flags(r)), t) ); } function pathToRegexp(e, t, r) { return ( isarray(t) || ((r = t || r), (t = [])), (r = r || {}), e instanceof RegExp ? regexpToRegexp(e, t) : isarray(e) ? arrayToRegexp(e, t, r) : stringToRegexp(e, t, r) ); } var PATH_REGEXP = new RegExp( [ '(\\\\.)', '([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))' ].join('|'), 'g' ); export { pathToRegexp, parse, compile, tokensToFunction, tokensToRegExp }; ================================================ FILE: packages/evershop/src/lib/postgres/connection.ts ================================================ import fs from 'fs'; import { PoolClient } from '@evershop/postgres-query-builder'; import { Pool } from 'pg'; import type { PoolConfig } from 'pg'; import { getConfig } from '../util/getConfig.js'; // Use env for the database connection, maintain the backward compatibility const connectionSetting: PoolConfig = { host: process.env.DB_HOST, port: process.env.DB_PORT as unknown as number, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, max: 20 }; // Support SSL const sslMode = process.env.DB_SSLMODE; switch (sslMode) { case 'disable': { connectionSetting.ssl = false; break; } case 'require': case 'prefer': case 'verify-ca': case 'verify-full': { const ssl: PoolConfig['ssl'] = { rejectUnauthorized: true }; const ca = process.env.DB_SSLROOTCERT; if (ca) { ssl.ca = fs.readFileSync(ca).toString(); } const cert = process.env.DB_SSLCERT; if (cert) { ssl.cert = fs.readFileSync(cert).toString(); } const key = process.env.DB_SSLKEY; if (key) { ssl.key = fs.readFileSync(key).toString(); } connectionSetting.ssl = ssl; break; } case 'no-verify': { connectionSetting.ssl = { rejectUnauthorized: false }; break; } default: { connectionSetting.ssl = false; break; } } const pool = new Pool(connectionSetting); // Set the timezone pool.on('connect', (client) => { const timeZone = getConfig('shop.timezone', 'UTC'); client.query(`SET TIMEZONE TO "${timeZone}";`); }); async function getConnection(): Promise { return await pool.connect(); } export { pool, getConnection }; ================================================ FILE: packages/evershop/src/lib/response/render.ts ================================================ import fs from 'fs'; import path from 'path'; import { pathToFileURL } from 'url'; import jsesc from 'jsesc'; import { getNotifications } from '../../modules/base/services/notifications.js'; import { getPageMetaInfo } from '../../modules/cms/services/pageMetaInfo.js'; import { Config } from '../../types/appContext.js'; import { EvershopRequest } from '../../types/request.js'; import { EvershopResponse } from '../../types/response.js'; import { error } from '../log/logger.js'; import { get } from '../util/get.js'; import { getConfig } from '../util/getConfig.js'; import isProductionMode from '../util/isProductionMode.js'; import { processPreloadImages } from '../util/preloadScan.js'; import { getValueSync } from '../util/registry.js'; import { getRouteBuildPath } from '../webpack/getRouteBuildPath.js'; function normalizeAssets(assets) { if (typeof assets === 'object' && !Array.isArray(assets) && assets !== null) { return Object.values(assets); } return Array.isArray(assets) ? assets : [assets]; } function buildContextData( request: EvershopRequest, response: EvershopResponse ) { const pageMeta = getPageMetaInfo(request); const appConfig = getValueSync( 'appConfig', { tax: { priceIncludingTax: getConfig('pricing.tax.price_including_tax', false) }, catalog: { imageDimensions: { width: getConfig('catalog.product.image.width', 1200), height: getConfig('catalog.product.image.height', 1200) } }, pageMeta: pageMeta }, { request, response }, (value) => value && typeof value === 'object' && !Array.isArray(value) ); const config = Object.assign({}, appConfig, { pageMeta }); const contextValue = { graphqlResponse: get(response, 'locals.graphqlResponse', {}), config: config, propsMap: get(response, 'locals.propsMap', {}), widgets: get(response, 'locals.widgets', []), notifications: getNotifications(request) }; return contextValue; } function renderDevelopment( request: EvershopRequest, response: EvershopResponse ) { const route = request.currentRoute; const classes = route.isAdmin ? `admin ${route.id}` : `frontStore ${route.id}`; const language = getConfig('shop.language', 'en'); if (!route) { // In testing mode, we do not have devMiddleware response.send(` Sample Html Response `); return; } const contextValue = buildContextData(request, response); const safeContextValue = jsesc(contextValue, { json: true, isScriptContext: true }); const langCode = request.currentRoute?.isAdmin ? 'en' : language; const scriptPath = route.isAdmin ? '/backend/admin-main.js' : '/main.js'; response.send(`
    `); } function renderProduction(request, response) { const language = getConfig('shop.language', 'en'); const route = request.currentRoute; const langCode = route.isAdmin === true ? 'en' : language; const serverIndexPath = path.resolve( getRouteBuildPath(route), 'server', 'index.js' ); const assetsPath = path.resolve( getRouteBuildPath(route), 'client', 'index.json' ); const assets = JSON.parse(fs.readFileSync(assetsPath, 'utf8')); const cssList = [] as string[]; for (let i = 0; i < assets.css.length; i++) { const cssFilePath = path.resolve( getRouteBuildPath(route), 'client', path.basename(assets.css[i]) ); if (fs.existsSync(cssFilePath)) { const cssContent = fs.readFileSync(cssFilePath, 'utf8'); // Inline the css content to reduce the number of requests cssList.push(cssContent); } } const contextValue = buildContextData(request, response); const safeContextValue = jsesc(contextValue, { json: true, isScriptContext: true }); import(pathToFileURL(serverIndexPath).toString()) .then((module) => { const source = processPreloadImages( module.default( request.currentRoute, assets.js, cssList, safeContextValue, langCode ) ); response.send(source); }) .catch((e) => { error(e); }); } export function render(request, response) { if (isProductionMode()) { renderProduction(request, response); } else { renderDevelopment(request, response); } } ================================================ FILE: packages/evershop/src/lib/router/Router.js ================================================ import { sortRoutes } from './sortRoutes.js'; class Router { constructor() { this.routes = []; } getFrontStoreRoutes() { return this.routes.filter((r) => r.isAdmin === false); } getAdminRoutes() { return this.routes.filter((r) => r.isAdmin === true); } getRoutes() { return sortRoutes(this.routes); } addRoute(route) { const r = this.routes.find((rt) => rt.id === route.id); if (r !== undefined) { Object.assign(r, route); } else { this.routes.push(route); } } hasRoute(id) { return this.routes.some((r) => r.id === id); } deleteRoute(id) { this.routes = this.routes.filter((r) => r.id !== id); } empty() { this.routes = []; } } const router = new Router(); export const addRoute = (route) => router.addRoute(route); export const getFrontStoreRoutes = () => router.getFrontStoreRoutes(); export const getAdminRoutes = () => router.getAdminRoutes(); export const getRoutes = () => router.getRoutes(); export const hasRoute = (id) => router.hasRoute(id); export const deleteRoute = (id) => router.deleteRoute(id); export const getRoute = (id) => router.getRoutes().find((r) => r.id === id); export const empty = () => router.empty(); ================================================ FILE: packages/evershop/src/lib/router/buildAbsoluteUrl.ts ================================================ import { normalizePort } from '../../bin/lib/normalizePort.js'; import { getBaseUrl } from '../../lib/util/getBaseUrl.js'; import { buildUrl } from './buildUrl.js'; const port = normalizePort(); /** * This function take a route ID, list of params and return the absolute url * * @param {string} routeId * @param {object} params Key-Pair value of route params * * @return {string} The Url */ export const buildAbsoluteUrl = ( routeId: string, params: Record = {} ) => { const url = buildUrl(routeId, params).replace(/^\/|\/$/g, ''); const homeUrl = getBaseUrl(); return `${homeUrl}/${url}`; }; ================================================ FILE: packages/evershop/src/lib/router/buildUrl.ts ================================================ import { compile } from '../pathToRegexp.js'; import { getRoutes } from './Router.js'; /** * This function take a route ID, list of params and return the url * * @param {string} routeId * @param {object} params Key-Pair value of route params * @param {object} query Key-Pair value of query parameters * * @return {string} The Url */ export const buildUrl = ( routeId: string, params: Record = {}, query: Record = {} ): string => { const routes = getRoutes(); const route = routes.find((r) => r.id === routeId); if (route === undefined) { throw new Error(`Route ${routeId} is not existed`); } const toPath = compile(route.path); try { const url = toPath(params); if (Object.keys(query).length > 0) { const queryPairs: string[] = []; for (const [key, value] of Object.entries(query)) { if (Array.isArray(value)) { value.forEach((item) => { queryPairs.push( `${encodeURIComponent(key)}[]=${encodeURIComponent(String(item))}` ); }); } else if (value !== null && value !== undefined) { // Handle simple values queryPairs.push( `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}` ); } } if (queryPairs.length > 0) { return `${url}?${queryPairs.join('&')}`; } } return url; } catch (e) { throw new Error(`Could not build url for route ${routeId}. ${e.message}`); } }; ================================================ FILE: packages/evershop/src/lib/router/index.ts ================================================ export { buildUrl } from './buildUrl.js'; export { buildAbsoluteUrl } from './buildAbsoluteUrl.js'; ================================================ FILE: packages/evershop/src/lib/router/loadModuleRoutes.js ================================================ import { existsSync } from 'fs'; import path from 'path'; import { registerAdminRoute } from './registerAdminRoute.js'; import { registerFrontStoreRoute } from './registerFrontStoreRoute.js'; import { scanForRoutes } from './scanForRoutes.js'; export const loadModuleRoutes = (modulePath) => { // Check for routes if (existsSync(path.resolve(modulePath, 'pages', 'admin'))) { const adminControllerRoutes = scanForRoutes( path.resolve(modulePath, 'pages', 'admin'), true, false ); adminControllerRoutes.forEach((route) => { registerAdminRoute( route.id, route.method, route.path, route.name, route.isApi, route.folder ); }); } if (existsSync(path.resolve(modulePath, 'pages', 'frontStore'))) { const frontStoreControllerRoutes = scanForRoutes( path.resolve(modulePath, 'pages', 'frontStore'), false, false ); frontStoreControllerRoutes.forEach((route) => { registerFrontStoreRoute( route.id, route.method, route.path, route.name, route.isApi, route.folder ); }); } // Wiwth API, we do not have admin and frontStore folders if (existsSync(path.resolve(modulePath, 'api'))) { const routes = scanForRoutes(path.resolve(modulePath, 'api'), false, true); routes.forEach((route) => { registerFrontStoreRoute( route.id, route.method, route.path, route.name, route.isApi, route.folder, route.payloadSchema, route.access ); }); } }; ================================================ FILE: packages/evershop/src/lib/router/registerAdminRoute.js ================================================ import { addRoute } from './Router.js'; /** * Register an admin route * * @param {string} id Id of route, this must be unique * @param {string|array} method HTTP method, can be string like "GET", array like ["GET", "POST"] * @param {string} path The path of route * */ export function registerAdminRoute( id, method, path, name, isApi = false, folder = '' ) { // const route = validateRoute(id, method, path); const route = { id: String(id), method, path }; route.isAdmin = true; route.isApi = isApi; route.path = route.path === '/' ? '/admin' : `/admin${path}`; route.folder = folder; route.name = name; addRoute(route); } ================================================ FILE: packages/evershop/src/lib/router/registerFrontStoreRoute.js ================================================ import { addRoute } from './Router.js'; /** * Register a frontStore route * * @param {string} id Id of route, this must be unique * @param {string|array} method HTTP method, can be string like "GET", array like ["GET", "POST"] * @param {string} path The path of route * */ export function registerFrontStoreRoute( id, method, path, name, isApi = false, folder = '', payloadSchema = null, access = 'private' ) { // const route = validateRoute(id, method, path); const route = { id: String(id), method, path, payloadSchema, access }; route.isAdmin = false; route.isApi = isApi; route.folder = folder; route.name = name; addRoute(route); } ================================================ FILE: packages/evershop/src/lib/router/scanForRoutes.js ================================================ import { existsSync, readdirSync } from 'fs'; import { basename, dirname, join } from 'path'; import { jsonParse } from '../util/jsonParse.js'; function startWith(str, prefix) { return str.slice(0, prefix.length) === prefix; } function validateRoute(methods, path, routePath) { if (methods.length === 0) { throw new Error( `Method is required. Please check the route defined at ${routePath}` ); } const check = methods.find( (m) => ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'].includes( m ) === false ); if (check !== undefined) { throw new Error( `Method ${check} is invalid. Please check the route defined at ${routePath}` ); } if (startWith(path, '/') === false) { throw new Error( `Path ${path} must be started with '/'. Please check the route defined at ${routePath}` ); } return true; } export function parseRoute(jsonPath, isAdmin = false, isApi = false) { const routeId = basename(dirname(jsonPath)); if (/^[a-zA-Z]+$/.test(routeId) === false) { throw new Error( `Route folder ${routeId} is invalid. It must contains only characters.` ); } const routeJson = jsonParse(jsonPath); const methods = routeJson?.methods.map((m) => m.toUpperCase()) || []; let routePath = routeJson?.path; if (validateRoute(methods, routePath, routePath) === true) { if (isApi === true) { routePath = `/api${routePath}`; } // Load the validation schema let payloadSchema; if (existsSync(join(dirname(jsonPath), 'payloadSchema.json'))) { payloadSchema = jsonParse(join(dirname(jsonPath), 'payloadSchema.json')); } return { id: routeId, name: routeJson?.name || routeId, method: methods, path: routePath, isAdmin, isApi, folder: dirname(jsonPath), payloadSchema, access: routeJson?.access || 'private' }; } else { return null; } } /** * Scan for routes base on module path. */ export function scanForRoutes(path, isAdmin, isApi) { const scanedRoutes = readdirSync(path, { withFileTypes: true }) .filter((dirent) => dirent.isDirectory()) .map((dirent) => dirent.name); return scanedRoutes .map((r) => { if (/^[A-Za-z.]+$/.test(r) === true) { if (existsSync(join(path, r, 'route.json'))) { return ( parseRoute(join(path, r, 'route.json'), isAdmin, isApi) || false ); } else { return false; } } else { return false; } }) .filter((e) => e !== false); } ================================================ FILE: packages/evershop/src/lib/router/sortRoutes.js ================================================ export function sortRoutes(routes) { return routes.sort((a, b) => { const aSpecificity = calculateRouteSpecificity(a.path); const bSpecificity = calculateRouteSpecificity(b.path); return bSpecificity - aSpecificity; }); } function calculateRouteSpecificity(path) { let specificity = 0; if (!path.includes(':')) { specificity += 100; } specificity += path .split('/') .filter((segment) => !segment.startsWith(':')).length; specificity -= path .split('/') .filter((segment) => segment.startsWith(':')).length; if (path.includes('(') && path.includes(')')) { specificity -= 5; } return specificity; } ================================================ FILE: packages/evershop/src/lib/router/tests/unit/a/invalidMethod/routeOne/route.json ================================================ { "methods": ["GET", "INVALID"], "path": "/a/:url_key" } ================================================ FILE: packages/evershop/src/lib/router/tests/unit/a/invalidPath/routeTwo/route.json ================================================ { "methods": ["GET", "POST"], "path": "invalidPath" } ================================================ FILE: packages/evershop/src/lib/router/tests/unit/b/routeOne/route.json ================================================ { "methods": ["GET", "POST"], "path": "/a/:url_key" } ================================================ FILE: packages/evershop/src/lib/router/tests/unit/b/routeThree/route.json ================================================ { "methods": ["GET", "PUT"], "path": "/b/:url_key" } ================================================ FILE: packages/evershop/src/lib/router/tests/unit/b/routeTwo/route.json ================================================ { "methods": ["GET"], "path": "/a/:url_key" } ================================================ FILE: packages/evershop/src/lib/router/tests/unit/unit.scanForRoutes.test.js ================================================ import { scanForRoutes } from '../../scanForRoutes.js'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); describe('Test scanForRoutes', () => { it('It should thrown an exception if path is not valid', () => { expect(() => scanForRoutes(path.resolve(__dirname, 'a/invalidPath'), true, false) ).toThrow(Error); }); it('It should thrown an exception if methods are not valid', () => { expect(() => scanForRoutes(path.resolve(__dirname, 'a/invalidMethod'), true, false) ).toThrow(Error); }); it('It should return an array of routes', () => { const routes = scanForRoutes(path.resolve(__dirname, 'b'), true, false); expect(Array.isArray(routes)).toBe(true); expect(routes.length).toBe(3); }); }); ================================================ FILE: packages/evershop/src/lib/router/tests/unit/unit.validateRoute.test.js ================================================ import path from 'path'; import { scanForRoutes } from '../../scanForRoutes.js'; import { registerAdminRoute } from '../../registerAdminRoute.js'; import { validateRoute } from '../../validateRoute.js'; import { jest, describe, it, expect, beforeAll, afterAll } from '@jest/globals'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); describe('Test validateRoute', () => { beforeAll(() => { const routes = scanForRoutes(path.resolve(__dirname, 'b'), true, false); routes.forEach((route) => { registerAdminRoute( route.id, route.method, route.path, route.name, route.isApi ); }); }); it('It should thrown an exception if route is already existed', () => { expect(() => validateRoute('routeOne', ['GET'], '/')).toThrow(Error); }); it('It should return a route object if id is valid', () => { const route = validateRoute('newRoute', ['GET'], '/'); expect(route.id).toBeTruthy(); expect(route.method).toBeTruthy(); expect(route.path).toBeTruthy(); }); }); ================================================ FILE: packages/evershop/src/lib/router/validateRoute.js ================================================ import { getRoutes } from './Router.js'; /** * This function validates if the id of a route already exists or not. * It returns a route object * * @param {string} id Route ID * @param {string||array} method HTTP method, can be string like "GET", array like ["GET", "POST"] * @param {string} path The route path * * @return {object} The Route object */ export const validateRoute = (id, method, path) => { const routes = getRoutes(); if (routes.find((r) => r.id === id) !== undefined) { throw new Error(`Route with ID ${String(id)} already exists`); } return { id: String(id), method, path }; }; ================================================ FILE: packages/evershop/src/lib/util/assign.js ================================================ /** * This function take 2 objects and merge the second one to the first one * * @param {object} object The main object * @param {object} data The object to be merged * */ export function assign(object, data) { if (typeof object !== 'object' || object === null) { throw new Error('`object` must be an object'); } if (typeof data !== 'object' || data === null) { throw new Error('`data` must be an object'); } Object.keys(data).forEach((key) => { if ( data[key] && data[key].constructor === Array && object[key] && object[key].constructor === Array ) { object[key] = object[key].concat(data[key]); } else if ( typeof object[key] !== 'object' || typeof data[key] !== 'object' || object[key] === null ) { object[key] = data[key]; } else { assign(object[key], data[key]); } }); } ================================================ FILE: packages/evershop/src/lib/util/buildFilterFromUrl.ts ================================================ export interface FilterInput { key: string; operation: | 'eq' | 'neq' | 'gt' | 'gteq' | 'lt' | 'lteq' | 'like' | 'nlike' | 'in' | 'nin'; value: string; } /** * Build filter inputs from URL parameters * * Supports both complete URLs and relative URL paths (like request.originalUrl). * For relative URLs, a dummy domain is added to make them valid for URL parsing. * * @param url - Complete URL, relative URL path, or URL object * @returns Array of FilterInput objects * * @example * // Complete URL * buildFilterFromUrl('https://example.com/products?color=red&size=large') * * // Relative URL (from request.originalUrl) * buildFilterFromUrl('/products?color=red&size=large') * * // Multi-value filters * buildFilterFromUrl('/products?color=red&color=blue') // Creates 'in' operation * * // Complex filters * buildFilterFromUrl('/products?price[operation]=range&price[value]=10,100') */ export const buildFilterFromUrl = (url: string | URL): FilterInput[] => { let urlObj: URL; if (typeof url === 'string') { // Check if it's a relative URL (starts with / or doesn't contain ://) if (url.startsWith('/') || !url.includes('://')) { // Add dummy domain to make it a valid URL for URL constructor urlObj = new URL(url, 'https://example.com'); } else { // It's already a complete URL urlObj = new URL(url); } } else { // It's already a URL object urlObj = url; } // Get the search parameters const searchParams = urlObj.searchParams; if (!searchParams) { return []; } const filtersFromUrl: FilterInput[] = []; const processedKeys = new Set(); // Iterate through all search parameters for (const [key, value] of searchParams.entries()) { // Handle operation-based filters like price[operation]=range&price[value]=10,100 if (key.includes('[operation]')) { const filterKey = key.replace('[operation]', ''); const filterValue = searchParams.get(`${filterKey}[value]`); if (filterValue && isValidOperation(value)) { filtersFromUrl.push({ key: filterKey, operation: value as FilterInput['operation'], value: filterValue }); processedKeys.add(filterKey); } } // Handle simple equality filters like color=red&size=large // Also handle multi-value case like color=black&color=blue else if (!key.includes('[value]') && !processedKeys.has(key)) { const allValues = searchParams.getAll(key); if (allValues.length > 1) { // Multiple values for the same key, use 'in' operation filtersFromUrl.push({ key, operation: 'in', value: allValues.join(',') }); } else { // Single value, use 'eq' operation filtersFromUrl.push({ key, operation: 'eq', value }); } processedKeys.add(key); } } return filtersFromUrl; }; // Helper function to validate operation const isValidOperation = ( operation: string ): operation is FilterInput['operation'] => { return [ 'eq', 'neq', 'gt', 'gteq', 'lt', 'lteq', 'like', 'nlike', 'in', 'nin' ].includes(operation); }; ================================================ FILE: packages/evershop/src/lib/util/camelCase.ts ================================================ export const camelCase = (object: Record): Record => { // Throw error if the object is not an object if (typeof object !== 'object' && object !== null) { throw new Error('The object must be an object'); } const newObject: Record = {}; Object.keys(object).forEach((key) => { // Convert the key to camelCase const newKey = key.replace(/([-_][a-zA-Z0-9])/gi, ($1) => $1.toUpperCase().replace('-', '').replace('_', '') ); // Add the new key to the new object newObject[newKey] = object[key]; }); return newObject; }; ================================================ FILE: packages/evershop/src/lib/util/cn.ts ================================================ import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } ================================================ FILE: packages/evershop/src/lib/util/defaultPaginationFilters.js ================================================ import { CONSTANTS } from '../helpers.js'; import { getConfig } from './getConfig.js'; export const defaultPaginationFilters = [ { key: 'od', operation: ['eq'], callback: (query, operation, value, currentFilters) => { if (['ASC', 'DESC', 'asc', 'desc'].includes(value)) { query.orderDirection(value.toUpperCase()); currentFilters.push({ key: 'od', operation, value }); } } }, { key: 'page', operation: ['eq'], callback: (query, operation, value, currentFilters) => { if (parseInt(value, 10) > 0) { query.limit( (parseInt(value, 10) - 1) * CONSTANTS.ADMIN_COLLECTION_SIZE, CONSTANTS.ADMIN_COLLECTION_SIZE ); currentFilters.push({ key: 'page', operation, value }); } else { query.limit(0, CONSTANTS.ADMIN_COLLECTION_SIZE); currentFilters.push({ key: 'page', operation, value: 1 }); } } }, { key: 'limit', operation: ['eq'], callback: (query, operation, value, currentFilters) => { if (parseInt(value, 10) > 0) { // Get the current page from the current filters const page = currentFilters.find((f) => f.key === 'page'); if (page) { query.limit( (parseInt(page.value, 10) - 1) * parseInt(value, 10), parseInt(value, 10) ); } else { query.limit(0, parseInt(value, 10)); currentFilters.push({ key: 'limit', operation: 'eq', value }); } currentFilters.push({ key: 'limit', operation, value }); } else { currentFilters.push({ key: 'limit', operation, value: CONSTANTS.ADMIN_COLLECTION_SIZE }); } } }, { key: '*', operation: ['eq'], callback: function (query, operation, value, currentFilters) { const page = currentFilters.find((f) => f.key === 'page'); const limit = currentFilters.find((f) => f.key === 'limit'); const defaultPage = 1; const defaultLimit = this.isAdmin ? CONSTANTS.ADMIN_COLLECTION_SIZE : getConfig('catalog.collectionPageSize', 12); currentFilters.push({ key: 'page', operation: 'eq', value: defaultPage }); if (!limit) { currentFilters.push({ key: 'limit', operation: 'eq', value: defaultLimit }); } query.limit( (parseInt(page?.value || defaultPage, 10) - 1) * parseInt(limit?.value || defaultLimit, 10), parseInt(limit?.value || defaultLimit, 10) ); } } ]; ================================================ FILE: packages/evershop/src/lib/util/events.ts ================================================ export const FORM_VALIDATED = 'FORM_VALIDATED'; export const FORM_SUBMIT = 'FORM_SUBMIT'; export const FORM_FIELD_UPDATED = 'FORM_FIELD_UPDATED'; ================================================ FILE: packages/evershop/src/lib/util/filterOperationMap.ts ================================================ export enum SQLFilterOperation { eq = '=', neq = '<>', gt = '>', gteq = '>=', lt = '<', lteq = '<=', like = 'ILIKE', nlike = 'NOT ILIKE', in = 'IN', nin = 'NOT IN' } // Map the operation to the SQL operation export const OPERATION_MAP: Record = { eq: SQLFilterOperation.eq, neq: SQLFilterOperation.neq, gt: SQLFilterOperation.gt, gteq: SQLFilterOperation.gteq, lt: SQLFilterOperation.lt, lteq: SQLFilterOperation.lteq, like: SQLFilterOperation.like, nlike: SQLFilterOperation.nlike, in: SQLFilterOperation.in, nin: SQLFilterOperation.nin }; ================================================ FILE: packages/evershop/src/lib/util/formToJson.js ================================================ function update(data, keys, value) { if (keys.length === 0) { // Leaf node return value; } let key = keys.shift(); if (!key) { data = data || []; if (Array.isArray(data)) { key = data.length; } } // Try converting key to a numeric value const index = +key; if (!Number.isNaN(index)) { // We have a numeric index, make data a numeric array // This will not work if this is a associative array // with numeric keys data = data || []; key = index; } // If none of the above matched, we have an associative array data = data || {}; const val = update(data[key], keys, value); data[key] = val; return data; } export function serializeForm(formDataEntries, dataFilter) { const data = Array.from(formDataEntries).reduce((data, [field, value]) => { // eslint-disable-next-line prefer-const let [_, prefix, keys] = field.match(/^([^\[]+)((?:\[[^\]]*\])*)/); if (keys) { keys = Array.from(keys.matchAll(/\[([^\]]*)\]/g), (m) => m[1]); value = update(data[prefix], keys, value); } data[prefix] = value; return data; }, {}); // Check if dataFilter is a function if (typeof dataFilter === 'function') { return dataFilter(data); } else { return data; } } ================================================ FILE: packages/evershop/src/lib/util/get.ts ================================================ /** * Get the value base on the path * * @param {object} obj The Data object * @param {string} path The path of the property "a.b.c" * @param {any} defaultValue The default value in case the path is not existed * * @return {any} The value */ export function get( obj: Record | null | undefined, path: string, defaultValue?: D ): T | D { const pathSplit = path.split('.'); let current: any = obj; while (pathSplit.length) { if (typeof current !== 'object' || current === null) { return defaultValue as D; } const key = pathSplit.shift()!; if (!(key in current)) { return defaultValue as D; } current = current[key]; } return current === undefined || current === null ? (defaultValue as D) : current; } ================================================ FILE: packages/evershop/src/lib/util/getBaseUrl.ts ================================================ import { normalizePort } from '../../bin/lib/normalizePort.js'; import { getConfig } from './getConfig.js'; export function getBaseUrl(): string { const port = normalizePort(); const baseUrl = getConfig('shop.homeUrl', `http://localhost:${port}`); return baseUrl.replace(/\/+$/, ''); // Remove trailing slashes } ================================================ FILE: packages/evershop/src/lib/util/getConfig.ts ================================================ import config from 'config'; type ConfigStructure = { shop: { language: string; timezone: string; currency: string; weightUnit: string; homeUrl: string; }; system: { file_storage: string; admin_collection_size?: number; upload_allowed_mime_types: string[]; theme?: string; extensions: Array<{ name: string; resolve: string; enabled: boolean; }>; session: { maxAge: number; resave: boolean; saveUninitialized: boolean; cookieSecret: string; cookieName: string; adminCookieName: string; }; notification_emails: { from?: string; order_confirmation?: { enabled: boolean; templatePath?: string | null; [key: string]: unknown; }; customer_welcome?: { enabled: boolean; templatePath?: string | null; [key: string]: unknown; }; reset_password?: { enabled: boolean; templatePath?: string | null; [key: string]: unknown; }; }; stripe?: { secretKey?: string; publishableKey?: string; [key: string]: unknown; }; paypal?: { [key: string]: unknown; }; cod?: { status?: number; [key: string]: unknown; }; }; catalog: { collectionPageSize: number; product: { image: { width: number; height: number; }; }; showOutOfStockProduct: boolean; }; checkout: { showShippingNote: boolean; }; pricing: { rounding: string; precision: number; tax: { rounding: string; precision: number; round_level: string; price_including_tax: boolean; }; }; themeConfig: { logo: { alt: string | undefined; src: string | undefined; width: number | undefined; height: number | undefined; }; headTags: { links: any[]; metas: any[]; scripts: any[]; bases: any[]; }; copyRight: string; }; oms: { order: { shipmentStatus: Record< string, { name: string; badge: string; progress?: string; isDefault?: boolean; isCancelable?: boolean; } >; paymentStatus: Record< string, { name: string; badge: string; progress?: string; isDefault?: boolean; isCancelable?: boolean; } >; status: Record< string, { name: string; badge: string; progress?: string; isDefault?: boolean; next: string[]; } >; psoMapping: Record; reStockAfterCancellation: boolean; }; carriers: Record< string, { name: string; trackingUrl?: string; } >; }; }; type PathValue = P extends keyof T ? T[P] : P extends `${infer K}.${infer Rest}` ? K extends keyof T ? PathValue : never : never; type ConfigPath = | keyof ConfigStructure | { [K in keyof ConfigStructure]: K extends string ? | `${K}.${Extract}` | { [K2 in keyof ConfigStructure[K]]: K2 extends string ? | `${K}.${K2}.${Extract< keyof ConfigStructure[K][K2], string >}` | { [K3 in keyof ConfigStructure[K][K2]]: K3 extends string ? `${K}.${K2}.${K3}.${Extract< keyof ConfigStructure[K][K2][K3], string >}` : never; }[keyof ConfigStructure[K][K2]] : never; }[keyof ConfigStructure[K]] : never; }[keyof ConfigStructure]; /** * Get the configuration value base on path. Return the default value if the path is not found. */ export function getConfig

    ( path: P, defaultValue?: PathValue ): PathValue { return config.has(path as string) ? config.get>(path as string) : (defaultValue as PathValue); } ================================================ FILE: packages/evershop/src/lib/util/getEnabledTheme.ts ================================================ import { existsSync } from 'fs'; import { resolve } from 'path'; import { CONSTANTS } from '../helpers.js'; import { error } from '../log/logger.js'; import { getConfig } from './getConfig.js'; import isDevelopmentMode from './isDevelopmentMode.js'; import isProductionMode from './isProductionMode.js'; export type Theme = { name: string; path: string; srcPath?: string; }; export function getEnabledTheme(): Theme | null { const themeConfig = getConfig('system.theme') as string | undefined; if (!themeConfig) { return null; } if (!existsSync(resolve(CONSTANTS.THEMEPATH, themeConfig))) { error( `Theme '${themeConfig}' does not exist in ${CONSTANTS.THEMEPATH}. Please check your theme configuration in the system settings.` ); process.exit(1); } else if ( isDevelopmentMode() && !existsSync(resolve(CONSTANTS.THEMEPATH, themeConfig, 'src')) ) { error( `Theme '${themeConfig}' must have a 'src' directory at ${resolve( CONSTANTS.THEMEPATH, themeConfig, 'src' )}. This is required for development mode.` ); process.exit(1); } else if ( isProductionMode() && !existsSync(resolve(CONSTANTS.THEMEPATH, themeConfig, 'dist')) ) { error( `Theme '${themeConfig}' must have a 'dist' directory at ${resolve( CONSTANTS.THEMEPATH, themeConfig, 'dist' )}. This is required for production mode. Please run the compile command to generate the dist directory.` ); process.exit(1); } else { return { name: themeConfig, path: resolve(CONSTANTS.THEMEPATH, themeConfig), srcPath: isDevelopmentMode() ? resolve(CONSTANTS.THEMEPATH, themeConfig, 'src') : undefined }; } } ================================================ FILE: packages/evershop/src/lib/util/getEnv.ts ================================================ /** * Get environment variable value * * @param name - Environment variable name * @param defaultValue - Default value if environment variable is not set * @returns The environment variable value or default value */ export function getEnv(name: string, defaultValue?: string): string { const value = process.env[name]; return value !== undefined ? (value as string) : (defaultValue as string); } ================================================ FILE: packages/evershop/src/lib/util/hookable.ts ================================================ type Hook = { callback: Function; priority: number; }; export enum HookPosition { BEFORE = 'before', AFTER = 'after' } type HookStorage = Map; const beforeHooks: HookStorage = new Map(); const afterHooks: HookStorage = new Map(); let locked = false; function isAsyncFunction(func: Function): boolean { return func.constructor.name === 'AsyncFunction'; } function hook( funcName: string, callback: Function, priority: number = 10, position: HookPosition = HookPosition.BEFORE ): void { if (locked) { throw new Error( 'Hooks are locked. You should consider adding hooks using the bootstrap function' ); } if (typeof callback !== 'function') { throw new Error('Callback must be a function'); } if (typeof priority !== 'number') { throw new Error('Priority must be a number'); } const storage = position === HookPosition.BEFORE ? beforeHooks : afterHooks; if (!storage.has(funcName)) { storage.set(funcName, []); } const hooks = storage.get(funcName)!; hooks.push({ callback, priority }); hooks.sort((a, b) => a.priority - b.priority); } export function hookAfter< TContext = any, TResult = any, TArgs extends any[] = any[] >( funcName: string, callback: ( this: TContext, result: TResult, ...args: TArgs ) => void | Promise, priority: number = 10 ): void { hook(funcName, callback, priority, HookPosition.AFTER); } export function hookBefore( funcName: string, callback: (this: TContext, ...args: TArgs) => void | Promise, priority: number = 10 ): void { hook(funcName, callback, priority, HookPosition.BEFORE); } export function hookable( originalFunction: T, context?: any ): T { // Make sure the original function is a named function const funcName = originalFunction.name.replace('bound ', ''); if (!funcName) { throw new Error('The original function must be a named function'); } return new Proxy(originalFunction, { apply: isAsyncFunction(originalFunction) ? async function (target, thisArg, argumentsList) { const beforeHookFunctions = beforeHooks.get(funcName) || []; const afterHookFunctions = afterHooks.get(funcName) || []; for (let index = 0; index < beforeHookFunctions.length; index += 1) { const callbackFunc = beforeHookFunctions[index].callback; // Call the callback function with the cloned arguments await callbackFunc.call(context, ...argumentsList); } const result = await Reflect.apply(target, thisArg, argumentsList); for (let index = 0; index < afterHookFunctions.length; index += 1) { const callbackFunc = afterHookFunctions[index].callback; await callbackFunc.call(context, result, ...argumentsList); } return result; } : function (target, thisArg, argumentsList) { const beforeHookFunctions = beforeHooks.get(funcName) || []; const afterHookFunctions = afterHooks.get(funcName) || []; // Clone the argumentsList to avoid mutation beforeHookFunctions.forEach((hook) => { hook.callback.call(context, ...argumentsList); }); const result = Reflect.apply(target, thisArg, argumentsList); afterHookFunctions.forEach((hook) => { hook.callback.call(context, result, ...argumentsList); }); return result; } }) as T; } export function getHooks(): { beforeHooks: HookStorage; afterHooks: HookStorage; } { return { beforeHooks, afterHooks }; } export function clearHooks(): void { beforeHooks.clear(); afterHooks.clear(); } export function lockHooks(): void { locked = true; } ================================================ FILE: packages/evershop/src/lib/util/httpStatus.ts ================================================ export const OK = 200; export const INVALID_PAYLOAD = 400; export const INTERNAL_SERVER_ERROR = 500; export const NOT_FOUND = 404; export const UNAUTHORIZED = 401; export const FORBIDDEN = 403; export const CONFLICT = 409; export const UNPROCESSABLE_ENTITY = 422; export const TOO_MANY_REQUESTS = 429; export const GATEWAY_TIMEOUT = 504; ================================================ FILE: packages/evershop/src/lib/util/isAjax.ts ================================================ import type { EvershopRequest } from '../../types/request.js'; export function isAjax(request: EvershopRequest) { return request.get('X-Requested-With') === 'XMLHttpRequest'; } ================================================ FILE: packages/evershop/src/lib/util/isDevelopmentMode.ts ================================================ export default (): boolean => process.env.NODE_ENV === 'development'; export const isDevelopmentMode = (): boolean => process.env.NODE_ENV === 'development'; ================================================ FILE: packages/evershop/src/lib/util/isPlainObject.ts ================================================ /** * Checks if a value is a plain object (i.e., an object created by the Object constructor). * @param obj The object to check * @returns True if the value is a plain object, false otherwise */ export function isPlainObject(obj: unknown): boolean { if (typeof obj !== 'object' || obj === null) { return false; } const prototype = Object.getPrototypeOf(obj); return prototype === Object.prototype || prototype === null; } ================================================ FILE: packages/evershop/src/lib/util/isProductionMode.ts ================================================ export default (): boolean => process.env.NODE_ENV === 'production'; export const isProductionMode = (): boolean => process.env.NODE_ENV === 'production'; ================================================ FILE: packages/evershop/src/lib/util/isResolvable.ts ================================================ import * as fs from 'fs'; import * as path from 'path'; type AliasConfig = Record; /** * Checks whether a file exists for a given aliased path using the alias configuration. * * @param aliasedPath - The path starting with an alias (e.g., "@components/Button") * @param aliasConfig - The alias configuration mapping aliases to base paths * @returns true if the file exists or no alias matched; false if alias matched but file doesn't exist */ export function isResolvable( aliasedPath: string, aliasConfig: AliasConfig ): boolean { return true; for (const alias in aliasConfig) { if (aliasedPath.startsWith(alias)) { const relativePath = aliasedPath.slice(alias.length).replace(/^\/+/, ''); for (const basePath of aliasConfig[alias]) { const fullPath = path.join(basePath, `${relativePath}.jsx`); if (fs.existsSync(fullPath)) { return true; // File exists } } return false; // Alias matched, but no file found } } return true; // No alias matched } ================================================ FILE: packages/evershop/src/lib/util/jsonParse.ts ================================================ import { PathLike, readFileSync } from 'node:fs'; /** * Reads a JSON file and parses its content into an object. * * @param {PathLike} path - The path to the JSON file. * @returns {T} - The parsed object from the JSON file. * @throws {Error} - If the file cannot be read or the content is not valid JSON. */ export function jsonParse(path: PathLike): T { try { return JSON.parse(readFileSync(path, 'utf8')) as T; } catch (error) { throw new Error(`Failed to parse JSON from file ${path}: ${error.message}`); } } ================================================ FILE: packages/evershop/src/lib/util/jwt.ts ================================================ import jwt from 'jsonwebtoken'; import { CurrentCustomer, CurrentUser } from '../../types/request.js'; export const TOKEN_TYPES = { ADMIN: 'admin', CUSTOMER: 'customer' } as const; export type TokenType = (typeof TOKEN_TYPES)[keyof typeof TOKEN_TYPES]; /** * Get JWT configuration (lazy initialization) * This allows environment variables to be set after module import */ function getJwtConfig() { return { issuer: process.env.JWT_ISSUER || 'evershop', admin: { secret: process.env.JWT_ADMIN_SECRET, refreshSecret: process.env.JWT_ADMIN_REFRESH_SECRET, expiry: process.env.JWT_ADMIN_TOKEN_EXPIRY || 900, refreshExpiry: process.env.JWT_ADMIN_REFRESH_TOKEN_EXPIRY || 54000 }, customer: { secret: process.env.JWT_CUSTOMER_SECRET, refreshSecret: process.env.JWT_CUSTOMER_REFRESH_SECRET, expiry: process.env.JWT_CUSTOMER_TOKEN_EXPIRY || 1800, refreshExpiry: process.env.JWT_CUSTOMER_REFRESH_TOKEN_EXPIRY || 108000 } }; } interface DecodedAccessToken extends UserPayload, CustomerPayload { tokenType: TokenType; tokenKind: 'access'; iat: number; exp: number; aud: string; iss: string; } interface DecodedRefreshToken extends UserPayload, CustomerPayload { tokenType: TokenType; tokenKind: 'refresh'; iat: number; exp: number; aud: string; iss: string; } interface UserPayload { user: CurrentUser; } interface CustomerPayload { customer: CurrentCustomer; } /** * Generate JWT access token */ export function generateToken( payload: CustomerPayload | UserPayload, tokenType: TokenType = TOKEN_TYPES.CUSTOMER, expiresIn?: number ) { // Use separate secret for access tokens const jwtConfig = getJwtConfig(); const secret = jwtConfig[tokenType].secret; if (!secret) { throw new Error(`JWT secret for ${tokenType} is not configured`); } const defaultExpiry = tokenType === TOKEN_TYPES.ADMIN ? jwtConfig.admin.expiry : jwtConfig.customer.expiry; return jwt.sign( { ...payload, tokenType, tokenKind: 'access' // Explicitly mark as access token }, secret, { expiresIn: expiresIn || (defaultExpiry as number), issuer: jwtConfig.issuer, audience: tokenType } ); } /** * Verify JWT access token */ export function verifyToken(token: string, tokenType: TokenType) { const jwtConfig = getJwtConfig(); const secret = jwtConfig[tokenType].secret; if (!secret) { throw new Error(`JWT secret for ${tokenType} is not configured`); } try { const decoded = jwt.verify(token, secret, { issuer: jwtConfig.issuer, audience: tokenType }) as DecodedAccessToken; // Verify token type matches if (decoded.tokenType !== tokenType) { throw new Error(`Invalid token type. Expected ${tokenType}`); } // Verify this is an access token, not a refresh token if (decoded.tokenKind !== 'access') { throw new Error('Invalid token kind. Expected access token'); } return decoded; } catch (error: any) { if (error.name === 'TokenExpiredError') { throw new Error('Token has expired'); } else if (error.name === 'JsonWebTokenError') { throw new Error('Invalid token'); } throw error; } } /** * Generate refresh token (longer expiration, different secret) */ export function generateRefreshToken( payload: UserPayload | CustomerPayload, tokenType: TokenType = TOKEN_TYPES.CUSTOMER ) { // Use separate secret for refresh tokens const jwtConfig = getJwtConfig(); const secret = jwtConfig[tokenType].refreshSecret; if (!secret) { throw new Error(`JWT refresh secret for ${tokenType} is not configured`); } const refreshExpiry = tokenType === TOKEN_TYPES.ADMIN ? jwtConfig.admin.refreshExpiry : jwtConfig.customer.refreshExpiry; return jwt.sign( { ...payload, tokenType, tokenKind: 'refresh' // Explicitly mark as refresh token }, secret, { expiresIn: refreshExpiry as number, issuer: jwtConfig.issuer, audience: tokenType } ); } /** * Verify JWT refresh token */ export function verifyRefreshToken(token: string, tokenType: TokenType) { // Use separate secret for refresh tokens const jwtConfig = getJwtConfig(); const secret = jwtConfig[tokenType].refreshSecret; if (!secret) { throw new Error(`JWT refresh secret for ${tokenType} is not configured`); } try { const decoded = jwt.verify(token, secret, { issuer: jwtConfig.issuer, audience: tokenType }) as DecodedRefreshToken; // Verify token type matches if (decoded.tokenType !== tokenType) { throw new Error(`Invalid token type. Expected ${tokenType}`); } // Verify this is a refresh token, not an access token if (decoded.tokenKind !== 'refresh') { throw new Error('Invalid token kind. Expected refresh token'); } return decoded; } catch (error: any) { if (error.name === 'TokenExpiredError') { throw new Error('Refresh token has expired'); } else if (error.name === 'JsonWebTokenError') { throw new Error('Invalid refresh token'); } throw error; } } /** * Decode token without verification */ export function decodeToken( token: string ): DecodedAccessToken | DecodedRefreshToken | null { return jwt.decode(token, { json: true }) as | DecodedAccessToken | DecodedRefreshToken | null; } ================================================ FILE: packages/evershop/src/lib/util/keyGenerator.ts ================================================ // Simple hash function that works identically in both browser and Node.js function simpleHash(str: string): string { let hash = 0; if (str.length === 0) return hash.toString(16); for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32bit integer } // Convert to positive hex string with consistent length return Math.abs(hash).toString(16).padStart(8, '0'); } export function generateComponentKey(text: string): string { // Remove everything before '/src/' or '/dist/' const subPath = text.split('/src/')[1] || text.split('/dist/')[1]; if (!subPath) { return `e${simpleHash(text)}`; } return `e${simpleHash(subPath)}`; } ================================================ FILE: packages/evershop/src/lib/util/merge.js ================================================ /** * This function take 2 objects: target and source, and merge them together recursively * This function will not merge built-in objects like Date, RegExp, Map, Set * This function will overwrite the value from the first object by the value from the second object if they have the same key and the value is primitive * This function will merge array property from 2 objects * * @param {object} target The target object * @param {object} source The source object * * @return {object} The new target object */ export function merge(target, source, maxDepth = 20, currentDepth = 0) { function isBuiltInObject(obj) { return ( obj instanceof Date || obj instanceof RegExp || obj instanceof Map || obj instanceof Set ); } if (isBuiltInObject(target) || isBuiltInObject(source)) { throw new Error( 'deepMerge cannot merge built-in objects like Date or RegExp' ); } if ( typeof target !== 'object' || target === null || typeof source !== 'object' || source === null ) { throw new Error('merge function can only merge plain objects'); } if (currentDepth > maxDepth) { throw new Error(`Maximum depth of ${maxDepth} exceeded`); } function getAllKeys(obj) { let keys = []; Object.getOwnPropertyNames(obj).forEach((key) => { if (!keys.includes(key)) { keys.push(key); } }); keys = keys.concat(Object.getOwnPropertySymbols(obj)); return keys; } getAllKeys(source).forEach((key) => { if (key in target) { if (Array.isArray(target[key]) && Array.isArray(source[key])) { target[key] = Array.from(new Set([...target[key], ...source[key]])); } else if ( typeof target[key] === 'object' && typeof source[key] === 'object' ) { if (isBuiltInObject(target[key]) || isBuiltInObject(source[key])) { target[key] = source[key]; } else { merge(target[key], source[key], maxDepth, currentDepth + 1); } } else { target[key] = source[key]; } } else { Object.defineProperty( target, key, Object.getOwnPropertyDescriptor(source, key) ); } }); return target; } ================================================ FILE: packages/evershop/src/lib/util/parseImageSizes.ts ================================================ // Define your desired image breakpoints. Consider putting this in a config file. const deviceSizes = [320, 640, 750, 828, 1080, 1200, 1920, 2048, 3840]; const isValidCondition = (condition: string): boolean => { if (!condition || typeof condition !== 'string') { return false; } const trimmed = condition.trim(); if (!trimmed) { return false; } // Special case: handle 'auto' keyword if (trimmed === 'auto') { return true; } // Check for valid CSS units pattern - allow some whitespace between value and unit const validUnitsPattern = /(\d+(?:\.\d+)?)\s*(vw|vh|px|rem|em|%|ch|vmin|vmax|pt|pc|in|cm|mm|ex|ic|lh|vi|vb|cqw|cqh|cqi|cqb|cqmin|cqmax)\s*$/; // If it has parentheses, it should be a media query if (trimmed.includes('(')) { // Basic media query validation - must have closing parenthesis and valid value const hasClosingParen = trimmed.includes(')'); const hasValidValue = validUnitsPattern.test(trimmed); // Also check that it has proper media query structure const hasProperMediaQuery = /\([^)]*\)/.test(trimmed); return hasClosingParen && hasValidValue && hasProperMediaQuery; } else { // Simple value without media query - but shouldn't have unmatched closing parenthesis const hasUnmatchedClosingParen = trimmed.includes(')'); const hasValidValue = validUnitsPattern.test(trimmed); return !hasUnmatchedClosingParen && hasValidValue; } }; // Parse sizes string to estimate actual image sizes export const parseImageSizes = (sizes: string): number[] => { // Validate input if (!sizes || typeof sizes !== 'string') { throw new Error('Invalid sizes attribute: must be a non-empty string'); } const trimmedSizes = sizes.trim(); if (!trimmedSizes) { throw new Error( 'Invalid sizes attribute: cannot be empty or whitespace only' ); } // Handle fixed pixel values first if ( trimmedSizes.endsWith('px') && !trimmedSizes.includes(',') && !trimmedSizes.includes('(') ) { const pixelValue = parseInt(trimmedSizes); if (!isNaN(pixelValue) && pixelValue > 0) { // For fixed pixel values, generate a few sizes around that value return deviceSizes .filter((size) => size >= pixelValue * 0.5) // Include smaller sizes for efficiency .slice(0, 4); // Limit to 4 sizes to keep srcset reasonable } else { throw new Error( `Invalid pixel value in sizes attribute: "${trimmedSizes}" must be a positive number followed by "px"` ); } } // Parse complex sizes string with media queries const conditions = trimmedSizes .split(',') .map((s) => s.trim()) .filter(Boolean); if (conditions.length === 0) { throw new Error( 'Invalid sizes attribute: no valid conditions found after parsing' ); } // Validate that each condition has a proper format for (const condition of conditions) { if (!isValidCondition(condition)) { throw new Error( `Invalid condition in sizes attribute: "${condition}" - must contain a valid CSS length value or media query` ); } } // For each device size, determine what actual image size would be used const imageSizes = deviceSizes.map((deviceSize) => { // Go through conditions in order until we find a match for (const condition of conditions) { const result = evaluateCondition(condition, deviceSize); if (result !== null) { return result; } } // If no conditions matched, assume full width return deviceSize; }); // Remove duplicates, sort, and ensure we have reasonable variety const uniqueSizes = [...new Set(imageSizes)].sort((a, b) => a - b); // Ensure minimum variety for better responsive behavior if (uniqueSizes.length < 3) { // Add some intermediate sizes for better coverage const minSize = Math.min(...uniqueSizes); const maxSize = Math.max(...uniqueSizes); const midSize = Math.round((minSize + maxSize) / 2); uniqueSizes.push(midSize); } return [...new Set(uniqueSizes)].sort((a, b) => a - b); }; export const evaluateCondition = ( condition: string, deviceSize: number ): number | null => { // Remove extra whitespace condition = condition.trim(); // Check if this condition has a media query if (condition.includes('(')) { // Extract media query and value parts - comprehensive regex for all CSS units const mediaQueryMatch = condition.match(/\(([^)]+)\)/g); const valueMatch = condition.match( /(\d+(?:\.\d+)?)\s*(vw|vh|px|rem|em|%|ch|vmin|vmax|pt|pc|in|cm|mm|ex|ic|lh|vi|vb|cqw|cqh|cqi|cqb|cqmin|cqmax)\s*$/ ); if (!mediaQueryMatch || !valueMatch) { return null; } const mediaQueries = mediaQueryMatch.map((mq) => mq.slice(1, -1)); // Remove parentheses const value = parseFloat(valueMatch[1]); const unit = valueMatch[2]; // Check if all media queries match for this device size const allMatch = mediaQueries.every((mq) => { const matches = evaluateMediaQuery(mq, deviceSize); return matches; }); if (allMatch) { const result = convertToPixels(value, unit, deviceSize); return result; } return null; // Media query doesn't match } else { // Special case: handle 'auto' keyword if (condition.trim() === 'auto') { // For 'auto', use a reasonable default based on the device size // We'll use 25% of the viewport width as a reasonable approximation return Math.round(deviceSize * 0.25); } // No media query, this is a fallback value - comprehensive regex for all CSS units const valueMatch = condition.match( /(\d+(?:\.\d+)?)\s*(vw|vh|px|rem|em|%|ch|vmin|vmax|pt|pc|in|cm|mm|ex|ic|lh|vi|vb|cqw|cqh|cqi|cqb|cqmin|cqmax)\s*$/ ); if (valueMatch) { const value = parseFloat(valueMatch[1]); const unit = valueMatch[2]; const result = convertToPixels(value, unit, deviceSize); return result; } } return null; }; export const evaluateMediaQuery = ( mediaQuery: string, deviceSize: number ): boolean => { // Handle different media query types with improved regex patterns if (mediaQuery.includes('max-width')) { const maxWidth = parseFloat( mediaQuery.match(/max-width\s*:\s*(\d+(?:\.\d+)?)px/)?.[1] || '0' ); return deviceSize <= maxWidth; } if (mediaQuery.includes('min-width')) { const minWidth = parseFloat( mediaQuery.match(/min-width\s*:\s*(\d+(?:\.\d+)?)px/)?.[1] || '0' ); return deviceSize >= minWidth; } if (mediaQuery.includes('max-device-width')) { const maxDeviceWidth = parseFloat( mediaQuery.match(/max-device-width\s*:\s*(\d+(?:\.\d+)?)px/)?.[1] || '0' ); return deviceSize <= maxDeviceWidth; } if (mediaQuery.includes('min-device-width')) { const minDeviceWidth = parseFloat( mediaQuery.match(/min-device-width\s*:\s*(\d+(?:\.\d+)?)px/)?.[1] || '0' ); return deviceSize >= minDeviceWidth; } // Handle orientation if (mediaQuery.includes('orientation')) { if (mediaQuery.includes('landscape')) { return deviceSize >= 768; // Assume landscape for wider screens } if (mediaQuery.includes('portrait')) { return deviceSize < 768; // Assume portrait for narrower screens } } // Handle aspect-ratio (simplified) if (mediaQuery.includes('aspect-ratio')) { // For simplicity, assume most common aspect ratios match return true; } // Handle resolution/pixel density if ( mediaQuery.includes('resolution') || mediaQuery.includes('-webkit-device-pixel-ratio') ) { // For srcset calculation, we generally assume 2x displays are common return true; } // Handle prefers-color-scheme, prefers-reduced-motion etc. if (mediaQuery.includes('prefers-')) { // These don't affect image sizing, so return true return true; } // Default: if we can't parse it, assume it matches return true; }; export const convertToPixels = ( value: number, unit: string, deviceSize: number ): number => { switch (unit) { // Viewport units case 'vw': return Math.round((deviceSize * value) / 100); case 'vh': // Assume viewport height is roughly 1.5x viewport width for mobile, 0.6x for desktop const assumedHeight = deviceSize <= 768 ? deviceSize * 1.5 : deviceSize * 0.6; return Math.round((assumedHeight * value) / 100); case 'vmin': // vmin is the smaller of vw or vh const vminHeight = deviceSize <= 768 ? deviceSize * 1.5 : deviceSize * 0.6; const minDimension = Math.min(deviceSize, vminHeight); return Math.round((minDimension * value) / 100); case 'vmax': // vmax is the larger of vw or vh const vmaxHeight = deviceSize <= 768 ? deviceSize * 1.5 : deviceSize * 0.6; const maxDimension = Math.max(deviceSize, vmaxHeight); return Math.round((maxDimension * value) / 100); case 'vi': // Viewport inline (same as vw in horizontal writing mode) return Math.round((deviceSize * value) / 100); case 'vb': // Viewport block (same as vh in horizontal writing mode) const vbHeight = deviceSize <= 768 ? deviceSize * 1.5 : deviceSize * 0.6; return Math.round((vbHeight * value) / 100); // Absolute length units case 'px': return Math.round(value); case 'pt': // 1pt = 1.33px (approximately) return Math.round(value * 1.33); case 'pc': // 1pc = 16px (1 pica = 12 points) return Math.round(value * 16); case 'in': // 1in = 96px (CSS reference pixel) return Math.round(value * 96); case 'cm': // 1cm = 37.8px (96px/2.54) return Math.round(value * 37.8); case 'mm': // 1mm = 3.78px (37.8px/10) return Math.round(value * 3.78); // Relative length units case '%': // Assume % is relative to viewport width (same as vw in most contexts) return Math.round((deviceSize * value) / 100); case 'rem': // Assume 1rem = 16px (default browser font size) return Math.round(value * 16); case 'em': // Assume 1em = 16px (in absence of parent context) return Math.round(value * 16); case 'ex': // Assume 1ex = 8px (approximately 0.5em) return Math.round(value * 8); case 'ch': // Assume 1ch = 8px (approximate character width in monospace font) return Math.round(value * 8); case 'ic': // Assume 1ic = 16px (ideographic character width, similar to em) return Math.round(value * 16); case 'lh': // Assume 1lh = 24px (typical line height is 1.5em) return Math.round(value * 24); // Container query units (treat similar to viewport units for now) case 'cqw': // Container query width (fallback to viewport width) return Math.round((deviceSize * value) / 100); case 'cqh': // Container query height (fallback to viewport height) const cqhHeight = deviceSize <= 768 ? deviceSize * 1.5 : deviceSize * 0.6; return Math.round((cqhHeight * value) / 100); case 'cqi': // Container query inline (fallback to viewport width) return Math.round((deviceSize * value) / 100); case 'cqb': // Container query block (fallback to viewport height) const cqbHeight = deviceSize <= 768 ? deviceSize * 1.5 : deviceSize * 0.6; return Math.round((cqbHeight * value) / 100); case 'cqmin': // Container query min (fallback to vmin) const cqminHeight = deviceSize <= 768 ? deviceSize * 1.5 : deviceSize * 0.6; const cqMinDimension = Math.min(deviceSize, cqminHeight); return Math.round((cqMinDimension * value) / 100); case 'cqmax': // Container query max (fallback to vmax) const cqmaxHeight = deviceSize <= 768 ? deviceSize * 1.5 : deviceSize * 0.6; const cqMaxDimension = Math.max(deviceSize, cqmaxHeight); return Math.round((cqMaxDimension * value) / 100); default: // Fallback to treating as pixels return Math.round(value); } }; ================================================ FILE: packages/evershop/src/lib/util/passwordHelper.ts ================================================ import bcrypt from 'bcryptjs'; import { addProcessor, getValueSync } from './registry.js'; import { Validator, ValidatorManager } from './validator.js'; export function hashPassword(password: string): string { const salt = bcrypt.genSaltSync(10); const hash = bcrypt.hashSync(password, salt); return hash; } export function comparePassword(password: string, hash: string): boolean { return bcrypt.compareSync(password, hash); } export function verifyPassword(password: string): boolean { const validator = getValueSync>( 'passwordValidator', () => new ValidatorManager([ { id: 'passwordLength', func: (password) => password.length >= 6, errorMessage: 'Password must be at least 6 characters' } ]), {}, (value) => value instanceof ValidatorManager ); const { valid, errors } = validator.validateSync(password); if (!valid) { throw new Error(`${errors.join('\n\r')}`); } return true; } export function addPasswordValidationRule(rule: Validator): void { addProcessor('passwordValidator', (validatorManager) => { if (validatorManager instanceof ValidatorManager) { validatorManager.add(rule); return validatorManager; } else { throw new Error( 'passwordValidator must be an instance of ValidatorManager' ); } }); } ================================================ FILE: packages/evershop/src/lib/util/preloadScan.ts ================================================ interface PreloadImage { src: string; srcset?: string; sizes?: string; crossorigin?: string; } function extractPreloadImages(html: string): PreloadImage[] { const imgRegex = /]*itemProp=["']preload["'][^>]*>/gi; const images: PreloadImage[] = []; let match; while ((match = imgRegex.exec(html)) !== null) { const imgTag = match[0]; // Extract src attribute const srcMatch = imgTag.match(/src=["']([^"']*)["']/i); if (!srcMatch) continue; const preloadImage: PreloadImage = { src: srcMatch[1] }; const srcsetMatch = imgTag.match(/srcSet=["']([^"']*)["']/i); if (srcsetMatch) { preloadImage.srcset = srcsetMatch[1]; } const sizesMatch = imgTag.match(/sizes=["']([^"']*)["']/i); if (sizesMatch) { preloadImage.sizes = sizesMatch[1]; } const crossoriginMatch = imgTag.match(/crossorigin=["']([^"']*)["']/i); if (crossoriginMatch) { preloadImage.crossorigin = crossoriginMatch[1]; } images.push(preloadImage); } return images; } function generatePreloadLinks(images: PreloadImage[]): string { return images .map((image) => { const attributes = [ 'rel="preload"', 'as="image"', `href="${image.src}"`, 'fetchpriority="high"' ]; if (image.srcset) { attributes.push(`imagesrcset="${image.srcset}"`); } if (image.sizes) { attributes.push(`imagesizes="${image.sizes}"`); } if (image.crossorigin) { attributes.push(`crossorigin="${image.crossorigin}"`); } return ` `; }) .join('\n'); } export function injectPreloadLinks(html: string): string { const preloadImages = extractPreloadImages(html); if (preloadImages.length === 0) { return html; } const preloadLinks = generatePreloadLinks(preloadImages); const headCloseRegex = /(<\/head>)/i; const modifiedHtml = html.replace(headCloseRegex, `${preloadLinks}\n$1`); return modifiedHtml; } export function injectPreloadLinksAfterCharset(html: string): string { const preloadImages = extractPreloadImages(html); if (preloadImages.length === 0) { return html; } const preloadLinks = generatePreloadLinks(preloadImages); const charsetRegex = /(]*>)/i; if (charsetRegex.test(html)) { const modifiedHtml = html.replace(charsetRegex, `$1\n${preloadLinks}`); return modifiedHtml; } return injectPreloadLinks(html); } export function cleanupPreloadAttributes(html: string): string { return html.replace(/\s*itemProp=["']preload["']/gi, ''); } export function processPreloadImages(html: string): string { let processedHtml = injectPreloadLinksAfterCharset(html); processedHtml = cleanupPreloadAttributes(processedHtml); return processedHtml; } ================================================ FILE: packages/evershop/src/lib/util/readCsvFile.ts ================================================ import fs, { PathLike } from 'fs'; import csv from 'csv-parser'; export async function readCsvFile( filePath: PathLike | string ): Promise> { return new Promise((resolve, reject) => { const results: Record = {}; fs.createReadStream(filePath) .pipe(csv({ headers: false })) .on('data', (data) => { // Skip the first row (headers) if (!data[0].startsWith('#')) { results[data[0]] = data[1] as T; } }) .on('end', () => { resolve(results); }) .on('error', (err) => { reject(err); }); }); } ================================================ FILE: packages/evershop/src/lib/util/registry.ts ================================================ import isEqual from 'react-fast-compare'; let locked = false; export type RegistryValue = { initValue: T; context: Record; value: T; processors: { callback: SyncProcessor | AsyncProcessor; priority: number; }[]; }; export type SyncProcessor = (value: T) => T; export type AsyncProcessor = (value: T) => Promise; class Registry { values: Record>> = {}; async get( name: string, initValue: T, context?: Record, validator?: (value: T) => boolean ) { if (this.values[name]) { // If the initValue and the context are identical, return the cached value. Skip the processors if ( isEqual(initValue, this.values[name].initValue) && isEqual(this.values[name].context, context) && Object.prototype.hasOwnProperty.call(this.values[name], 'value') ) { return this.values[name].value; } } // Cache the initValue and the context this.values[name] = this.values[name] || ({} as RegistryValue); this.values[name].initValue = initValue; this.values[name].context = context; // If there is no processor, return the init value if (!this.values[name].processors) { this.values[name].value = initValue; return initValue; } const { processors } = this.values[name]; // Call the list of processors, returned value will be passed to the next processor. Start with the init value let value = initValue; for (let i = 0; i < processors.length; i += 1) { const { callback } = processors[i]; value = await callback.call(context, value); if (value === undefined) { // eslint-disable-next-line no-console console.log( `\x1b[33m⚠️ The processor for the value '${name}' is not returning anything. This may cause unexpected behavior.\x1b[0m` ); } // Validate the value if the validator is provided and it is a function if (typeof validator === 'function') { const validateResult = validator(value); if (validateResult !== true) { throw new Error(`Value ${name} is invalid: ${validateResult}`); } } } // Cache the value this.values[name].value = value; return value; } getSync( name: string, initValue: T, context?: Record, validator?: (value: T) => boolean ) { const validateFunc = (value: T) => { // Check if value is a promise if ( value !== null && typeof value === 'object' && typeof (value as unknown as Promise).then === 'function' ) { throw new Error( `The 'getSync' function does not support async processor. Please use 'get' function instead` ); } else if (typeof validator === 'function') { return validator(value); } else { return true; } }; if (this.values[name]) { // If the initValue and the context are identical, return the cached value. Skip the processors if ( isEqual(initValue, this.values[name].initValue) && isEqual(this.values[name].context, context) && Object.prototype.hasOwnProperty.call(this.values[name], 'value') ) { return this.values[name].value; } } // Cache the initValue and the context this.values[name] = this.values[name] || ({} as RegistryValue); this.values[name].initValue = initValue; this.values[name].context = context; // If there is no processor, return the init value if (!this.values[name].processors) { this.values[name].value = initValue; return initValue; } const { processors } = this.values[name]; // Call the list of processors, returned value will be passed to the next processor. Start with the init value let value = initValue; for (let i = 0; i < processors.length; i += 1) { const { callback } = processors[i]; value = callback.call(context, value); // Check if the callback function not returning anything if (value === undefined) { // eslint-disable-next-line no-console console.log( `\x1b[33m⚠️ The processor for the value '${name}' is not returning anything. This may cause unexpected behavior.\x1b[0m` ); } // Validate the value if the validator is provided and it is a function const validateResult = validateFunc(value); if (validateResult !== true) { throw new Error(`Value ${name} is invalid`); } } // Cache the value this.values[name].value = value; return value; } addProcessor( name: string, callback: SyncProcessor | AsyncProcessor, priority?: number ) { if (locked) { throw new Error( 'Registry is locked. Most likely you are trying to add a processor from a middleware. Consider using a bootstrap file to add processors' ); } if (typeof priority === 'undefined') { priority = 10; } // Throw error if priority is not a number if (typeof priority !== 'number') { throw new Error('Priority must be a number'); } // Throw error if the priority is bigger than 1000 if (priority > 1000) { throw new Error('Priority must be smaller than 1000'); } // Throw error if callback is not a function or async function if (typeof callback !== 'function') { throw new Error('Callback must be a function'); } if (!this.values[name]) { this.values[name] = { processors: [] } as Partial>; } this.values[name].processors = this.values[name].processors || []; // Add the callback to the processors, sort by priority const { processors } = this.values[name]; processors.push({ callback, priority }); processors.sort((a, b) => a.priority - b.priority); } addFinalProcessor( name: string, callback: SyncProcessor | AsyncProcessor ): void { // Check if there is already a final processor base on the priority const processors = this.values[name]?.processors || []; if (processors.find((p) => p.priority === 1000)) { throw new Error( `There is already a final processor for the value ${name}` ); } this.addProcessor(name, callback, 1000); } getProcessors(name: string): { callback: SyncProcessor | AsyncProcessor; priority: number; }[] { if (!this.values[name]) { throw new Error(`The value ${name} is not registered`); } return this.values[name].processors || []; } } const registry = new Registry(); /** * Get the value from the registry * @param name - The name of the value * @param initialization - The initialization value or a function that returns the value * @param context - The context of the value * @param validator - The validator function * @returns The value from the registry */ export async function getValue( name: string, initialization: T | AsyncProcessor | SyncProcessor, context?: Record, validator?: (value: T) => boolean ): Promise { let initValue; const value = registry.values[name] || ({} as RegistryValue); // Check if the initValue is a function, then add this function to the processors as the first processor if (typeof initialization === 'function') { // Add this function to the biginning of the processors const processors = value.processors || []; processors.unshift({ callback: initialization as SyncProcessor | AsyncProcessor, priority: 0 }); registry.values[name] = { ...value, processors }; initValue = value.initValue; } else { initValue = initialization as T; } const val = await registry.get(name, initValue, context, validator); return val; } /** * Get the value from the registry * @param name - The name of the value * @param initialization - The initialization value or a function that returns the value * @param context - The context of the value * @param validator - The validator function * @returns The value from the registry */ export function getValueSync( name: string, initialization: T | SyncProcessor, context: Record, validator?: (value: T) => boolean ): T { let initValue; // Check if the initValue is a function, then add this function to the processors as the first processor if (typeof initialization === 'function') { // Add this function to the processors, add this to the biginning of the processors const processors = registry.values[name]?.processors || []; processors.unshift({ callback: initialization as SyncProcessor, priority: 0 }); registry.values[name] = registry.values[name] || ({} as RegistryValue); registry.values[name].processors = processors; initValue = registry.values[name].initValue; } else { initValue = initialization; } const val = registry.getSync(name, initValue, context, validator); return val; } export function addProcessor( name: string, callback: SyncProcessor | AsyncProcessor, priority?: number ): void { return registry.addProcessor(name, callback, priority); } export function addFinalProcessor( name: string, callback: SyncProcessor | AsyncProcessor ): void { return registry.addFinalProcessor(name, callback); } export function getProcessors(name: string): { callback: SyncProcessor | AsyncProcessor; priority: number; }[] { return registry.getProcessors(name); } export function lockRegistry(): void { // Reset the values cache by removing all values from all properties in the registry values Object.keys(registry.values).forEach((key) => { if (Object.prototype.hasOwnProperty.call(registry.values, key)) { delete registry.values[key].value; } }); locked = true; } ================================================ FILE: packages/evershop/src/lib/util/sanitizeHtml.ts ================================================ import sanitizeHtml from 'sanitize-html'; // Extend the default allowed attributes to preserve CSS classes on all tags const sanitizeOptions: sanitizeHtml.IOptions = { allowedTags: sanitizeHtml.defaults.allowedTags.concat([ 'img', 'figure', 'figcaption', 'video', 'source', 'iframe' ]), allowedAttributes: { ...sanitizeHtml.defaults.allowedAttributes, '*': ['class', 'id', 'style'] } }; export interface Row { id: string; size: number; columns: { id: string; size: number; data: any; }[]; } /** * Sanitizes the HTML content in all EditorJS raw HTML blocks within the page content. * Each column's `data` is an EditorJS block: { type, data: { ... } }. * For "raw" type blocks, `data.html` is sanitized via sanitize-html. */ function sanitizeRawHtml(editorJSData: Row[]) { if (!Array.isArray(editorJSData)) { return; } editorJSData.forEach((row) => { if (!Array.isArray(row.columns)) { return; } row.columns.forEach((column) => { if (!column.data || !Array.isArray(column.data.blocks)) { return; } column.data.blocks.forEach((block) => { if ( block.type === 'raw' && block.data && typeof block.data.html === 'string' ) { block.data.html = sanitizeHtml(block.data.html, sanitizeOptions); } }); }); }); } export { sanitizeRawHtml }; ================================================ FILE: packages/evershop/src/lib/util/tests/unit/util.assign.test.js ================================================ import { assign } from '../../assign.js'; describe('assign', () => { it('It should assign an object to the main object', () => { const a = { a: 1 }; const b = { b: 1 }; assign(a, b); expect(a).toEqual({ a: 1, b: 1 }); }); it('It should thrown an exception if `object` is not an object or null', () => { const a = 1; const b = { b: 1 }; expect(() => assign(a, b)).toThrow(Error); }); it('It should thrown an exception if `object` is not an object or null', () => { const a = null; const b = { b: 1 }; expect(() => assign(a, b)).toThrow(Error); }); it('It should thrown an exception if data is not an object or null', () => { const a = { a: 1 }; const b = 1; expect(() => assign(a, b)).toThrow(Error); }); it('It should thrown an exception if data is not an object or null', () => { const a = { a: 1 }; const b = null; expect(() => assign(a, b)).toThrow(Error); }); it('It should overwrite if the property is already existed', () => { const a = { a: 1, b: 1 }; const b = { b: 2 }; assign(a, b); expect(a).toEqual({ a: 1, b: 2 }); }); }); ================================================ FILE: packages/evershop/src/lib/util/tests/unit/util.get.test.js ================================================ import { get } from '../../get.js'; describe('Test until get', () => { it('It should return the value if path is valid', () => { const object = { a: 1, b: { c: 1 } }; const result = get(object, 'b.c'); expect(result).toEqual(1); }); it('It should return undefined if the object is not an object', () => { const object = 1; expect(get(object, 'a.b')).toEqual(undefined); }); it('It should return undefined if the path is not found', () => { const object = { a: 1, b: { c: 1 } }; expect(get(object, 'a.b.c')).toEqual(undefined); }); it('It should return default value if the path is not found', () => { const object = { a: 1, b: { c: 1 } }; const defaultValue = 10; expect(get(object, 'a.b.d', defaultValue)).toEqual(10); }); }); ================================================ FILE: packages/evershop/src/lib/util/tests/unit/util.getConfig.test.js ================================================ import { getConfig } from '../../getConfig.js'; describe('Test until getConfig', () => { it('It should return the default value if path is invalid', () => { expect(getConfig('a.b', 1)).toEqual(1); expect(getConfig('a.b')).toEqual(undefined); }); }); ================================================ FILE: packages/evershop/src/lib/util/tests/unit/util.hookable.test.js ================================================ import { hookable, hookBefore, getHooks, clearHooks, lockHooks } from '../../hookable.js'; import { jest, describe, it, expect } from '@jest/globals'; describe('hookBefore', () => { it('It should add before hook to the registry', () => { const callback = () => {}; hookBefore('test', callback); const { beforeHooks } = getHooks(); expect(beforeHooks.get('test')).toEqual([ { callback, priority: 10 } ]); }); it('It should throw error if priority is not a number', () => { const callback = () => {}; expect(() => hookBefore('test', callback, 'abc')).toThrow(Error); }); it('It should add before hook to the registry with priority', () => { const negativeCallback = () => {}; const beforeCallback = () => {}; const afterCallback = () => {}; hookBefore('test2', beforeCallback, 5); hookBefore('test2', negativeCallback, -5); hookBefore('test2', afterCallback, 20); const { beforeHooks } = getHooks(); expect(JSON.stringify(beforeHooks.get('test2'))).toEqual( JSON.stringify([ { callback: negativeCallback, priority: -5 }, { callback: beforeCallback, priority: 5 }, { callback: afterCallback, priority: 20 } ]) ); }); }); describe('hookable', () => { it('It should throw error if the original function is not a named function', () => { expect(() => hookable(() => {})).toThrow(Error); }); it('It should return a function', () => { const func = function test() {}; expect(typeof hookable(func)).toEqual('function'); }); it('It should call the original function', () => { const func = jest.fn(); const hookedFunc = hookable(func); hookedFunc(); expect(func).toHaveBeenCalled(); }); it('It should throw error if one of the callback throws error', () => { const test = jest.fn(); hookBefore('mockConstructor', () => { throw new Error('Error'); }); const hookedFunc = hookable(test); expect(() => hookedFunc()).toThrow(Error); }); it('It should throw error if one of the callback throws error', async () => { const test = jest.fn(); hookBefore('mockConstructor', async () => { throw new Error('Error'); }); const hookedFunc = hookable(test); await expect(async () => await hookedFunc()).rejects.toThrow('Error'); }); it('It should call the before hook in correct order', () => { clearHooks(); const data = []; const test = jest.fn(); const beforeCallback1 = jest.fn(() => data.push(1)); const beforeCallback2 = jest.fn(() => data.push(2)); const beforeCallback3 = jest.fn(() => data.push(3)); hookBefore('mockConstructor', beforeCallback1); hookBefore('mockConstructor', beforeCallback2); hookBefore('mockConstructor', beforeCallback3, 1); const hookedFunc = hookable(test); hookedFunc(); expect(beforeCallback1).toHaveBeenCalled(); expect(beforeCallback2).toHaveBeenCalled(); expect(beforeCallback3).toHaveBeenCalled(); expect(data).toEqual([3, 1, 2]); }); it('It should call the before hook in correct order async', () => { clearHooks(); const data = []; const test = jest.fn(async () => new Promise((resolve) => resolve())); const beforeCallback1 = jest.fn( async () => new Promise((resolve) => resolve(data.push(1))) ); const beforeCallback2 = jest.fn( async () => new Promise((resolve) => resolve(data.push(2))) ); const beforeCallback3 = jest.fn( async () => new Promise((resolve) => resolve(data.push(3))) ); hookBefore('mockConstructor', beforeCallback1); hookBefore('mockConstructor', beforeCallback2); hookBefore('mockConstructor', beforeCallback3, 1); const hookedFunc = hookable(test); hookedFunc(); expect(beforeCallback1).toHaveBeenCalled(); expect(beforeCallback2).toHaveBeenCalled(); expect(beforeCallback3).toHaveBeenCalled(); expect(data).toEqual([3, 1, 2]); }); it('It should call the before hook in correct order async', async () => { clearHooks(); const data = []; const test = jest.fn( async () => new Promise((resolve) => { data.push(0); setTimeout(() => { data.push(4); resolve(); }, 1000); }) ); const beforeCallback1 = jest.fn( async () => new Promise((resolve) => resolve(data.push(1))) ); const beforeCallback2 = jest.fn( async () => new Promise((resolve) => resolve(data.push(2))) ); const beforeCallback3 = jest.fn(() => data.push(3)); hookBefore('mockConstructor', beforeCallback1); hookBefore('mockConstructor', beforeCallback2); hookBefore('mockConstructor', beforeCallback3); const hookedFunc = hookable(test); await hookedFunc(); expect(beforeCallback1).toHaveBeenCalled(); expect(beforeCallback2).toHaveBeenCalled(); expect(beforeCallback3).toHaveBeenCalled(); expect(data).toEqual([1, 2, 3, 0, 4]); }); it('It should call the original function with correct argument', () => { const test = jest.fn(); const hookedFunc = hookable(test); hookedFunc(1, 2, 3); expect(test).toHaveBeenCalledWith(1, 2, 3); }); it('It should call the callback with correct context', () => { const test = jest.fn(); const beforeCallback = jest.fn(function () { expect(this).toEqual({ test: 1 }); }); hookBefore('mockConstructor', beforeCallback); const hookedFunc = hookable(test, { test: 1 }); hookedFunc(); expect(beforeCallback).toHaveBeenCalled(); }); }); describe('lockHooks', () => { it('It should throw error if the hook is locked', () => { lockHooks(); const callback = () => {}; expect(() => hookBefore('test', callback)).toThrow(Error); }); }); ================================================ FILE: packages/evershop/src/lib/util/tests/unit/util.jwt.test.js ================================================ import { generateToken, generateRefreshToken, verifyToken, verifyRefreshToken, decodeToken, TOKEN_TYPES } from '../../jwt.js'; import { jest, describe, it, expect, beforeAll, afterAll } from '@jest/globals'; describe('JWT Utility Functions', () => { // Set up environment variables for testing beforeAll(() => { process.env.JWT_ISSUER = 'evershop-test'; process.env.JWT_ADMIN_SECRET = 'test-admin-secret-key-at-least-32-chars'; process.env.JWT_ADMIN_REFRESH_SECRET = 'test-admin-refresh-secret-key-at-least-32-chars'; process.env.JWT_ADMIN_TOKEN_EXPIRY = '3600'; // 1 hour in seconds process.env.JWT_ADMIN_REFRESH_TOKEN_EXPIRY = '604800'; // 7 days in seconds process.env.JWT_CUSTOMER_SECRET = 'test-customer-secret-key-at-least-32-chars'; process.env.JWT_CUSTOMER_REFRESH_SECRET = 'test-customer-refresh-secret-key-at-least-32-chars'; process.env.JWT_CUSTOMER_TOKEN_EXPIRY = '7200'; // 2 hours in seconds process.env.JWT_CUSTOMER_REFRESH_TOKEN_EXPIRY = '2592000'; // 30 days in seconds }); describe('generateToken', () => { it('should generate a valid admin access token', () => { const payload = { user: { userId: 1, email: 'admin@example.com', fullName: 'Admin User' } }; const token = generateToken(payload, TOKEN_TYPES.ADMIN); expect(token).toBeDefined(); expect(typeof token).toBe('string'); expect(token.split('.').length).toBe(3); // JWT has 3 parts }); it('should generate a valid customer access token', () => { const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; const token = generateToken(payload, TOKEN_TYPES.CUSTOMER); expect(token).toBeDefined(); expect(typeof token).toBe('string'); }); it('should include tokenType and tokenKind in payload', () => { const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; const token = generateToken(payload, TOKEN_TYPES.CUSTOMER); const decoded = decodeToken(token); expect(decoded.tokenType).toBe(TOKEN_TYPES.CUSTOMER); expect(decoded.tokenKind).toBe('access'); }); it('should throw error if secret is not configured', () => { const originalSecret = process.env.JWT_ADMIN_SECRET; delete process.env.JWT_ADMIN_SECRET; const payload = { user: { userId: 1, email: 'admin@example.com', fullName: 'Admin User' } }; expect(() => { generateToken(payload, TOKEN_TYPES.ADMIN); }).toThrow('JWT secret for admin is not configured'); process.env.JWT_ADMIN_SECRET = originalSecret; }); it('should use custom expiry when provided', () => { const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; const token = generateToken(payload, TOKEN_TYPES.CUSTOMER, 300); // 5 minutes const decoded = decodeToken(token); const expiryTime = decoded.exp - decoded.iat; expect(expiryTime).toBe(300); }); }); describe('verifyToken', () => { it('should verify a valid admin access token', () => { const payload = { user: { userId: 1, email: 'admin@example.com', fullName: 'Admin User' } }; const token = generateToken(payload, TOKEN_TYPES.ADMIN); const decoded = verifyToken(token, TOKEN_TYPES.ADMIN); expect(decoded).toBeDefined(); expect(decoded.user.userId).toBe(1); expect(decoded.user.email).toBe('admin@example.com'); expect(decoded.tokenType).toBe(TOKEN_TYPES.ADMIN); expect(decoded.tokenKind).toBe('access'); }); it('should verify a valid customer access token', () => { const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; const token = generateToken(payload, TOKEN_TYPES.CUSTOMER); const decoded = verifyToken(token, TOKEN_TYPES.CUSTOMER); expect(decoded).toBeDefined(); expect(decoded.customer.customerId).toBe(1); expect(decoded.customer.email).toBe('customer@example.com'); expect(decoded.tokenType).toBe(TOKEN_TYPES.CUSTOMER); }); it('should reject token with wrong token type', () => { const payload = { user: { userId: 1, email: 'admin@example.com', fullName: 'Admin User' } }; const adminToken = generateToken(payload, TOKEN_TYPES.ADMIN); expect(() => { verifyToken(adminToken, TOKEN_TYPES.CUSTOMER); }).toThrow('Invalid token'); }); it('should reject refresh token used as access token', () => { const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; const refreshToken = generateRefreshToken(payload, TOKEN_TYPES.CUSTOMER); expect(() => { verifyToken(refreshToken, TOKEN_TYPES.CUSTOMER); }).toThrow('Invalid token'); }); it('should reject token signed with wrong secret', () => { const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; const token = generateToken(payload, TOKEN_TYPES.CUSTOMER); // Try to verify with admin secret (wrong token type) expect(() => { verifyToken(token, TOKEN_TYPES.ADMIN); }).toThrow(); }); it('should reject expired token', (done) => { const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; // Generate token with 1 second expiry const token = generateToken(payload, TOKEN_TYPES.CUSTOMER, 1); // Wait for token to expire setTimeout(() => { expect(() => { verifyToken(token, TOKEN_TYPES.CUSTOMER); }).toThrow('Token has expired'); done(); }, 1500); }, 3000); it('should reject malformed token', () => { expect(() => { verifyToken('invalid.token.string', TOKEN_TYPES.CUSTOMER); }).toThrow('Invalid token'); }); it('should throw error if secret is not configured', () => { const originalSecret = process.env.JWT_CUSTOMER_SECRET; delete process.env.JWT_CUSTOMER_SECRET; const token = 'some.jwt.token'; expect(() => { verifyToken(token, TOKEN_TYPES.CUSTOMER); }).toThrow('JWT secret for customer is not configured'); process.env.JWT_CUSTOMER_SECRET = originalSecret; }); }); describe('generateRefreshToken', () => { it('should generate a valid admin refresh token', () => { const payload = { user: { userId: 1, email: 'admin@example.com', fullName: 'Admin User' } }; const token = generateRefreshToken(payload, TOKEN_TYPES.ADMIN); expect(token).toBeDefined(); expect(typeof token).toBe('string'); expect(token.split('.').length).toBe(3); }); it('should generate a valid customer refresh token', () => { const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; const token = generateRefreshToken(payload, TOKEN_TYPES.CUSTOMER); expect(token).toBeDefined(); expect(typeof token).toBe('string'); }); it('should include tokenKind as refresh in payload', () => { const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; const token = generateRefreshToken(payload, TOKEN_TYPES.CUSTOMER); const decoded = decodeToken(token); expect(decoded.tokenType).toBe(TOKEN_TYPES.CUSTOMER); expect(decoded.tokenKind).toBe('refresh'); }); it('should have longer expiry than access token', () => { const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; const accessToken = generateToken(payload, TOKEN_TYPES.CUSTOMER); const refreshToken = generateRefreshToken(payload, TOKEN_TYPES.CUSTOMER); const decodedAccess = decodeToken(accessToken); const decodedRefresh = decodeToken(refreshToken); const accessExpiry = decodedAccess.exp - decodedAccess.iat; const refreshExpiry = decodedRefresh.exp - decodedRefresh.iat; expect(refreshExpiry).toBeGreaterThan(accessExpiry); }); it('should throw error if refresh secret is not configured', () => { const originalSecret = process.env.JWT_ADMIN_REFRESH_SECRET; delete process.env.JWT_ADMIN_REFRESH_SECRET; const payload = { user: { userId: 1, email: 'admin@example.com', fullName: 'Admin User' } }; expect(() => { generateRefreshToken(payload, TOKEN_TYPES.ADMIN); }).toThrow('JWT refresh secret for admin is not configured'); process.env.JWT_ADMIN_REFRESH_SECRET = originalSecret; }); }); describe('verifyRefreshToken', () => { it('should verify a valid admin refresh token', () => { const payload = { user: { userId: 1, email: 'admin@example.com', fullName: 'Admin User' } }; const token = generateRefreshToken(payload, TOKEN_TYPES.ADMIN); const decoded = verifyRefreshToken(token, TOKEN_TYPES.ADMIN); expect(decoded).toBeDefined(); expect(decoded.user.userId).toBe(1); expect(decoded.tokenType).toBe(TOKEN_TYPES.ADMIN); expect(decoded.tokenKind).toBe('refresh'); }); it('should verify a valid customer refresh token', () => { const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; const token = generateRefreshToken(payload, TOKEN_TYPES.CUSTOMER); const decoded = verifyRefreshToken(token, TOKEN_TYPES.CUSTOMER); expect(decoded).toBeDefined(); expect(decoded.customer.customerId).toBe(1); expect(decoded.tokenKind).toBe('refresh'); }); it('should reject access token used as refresh token', () => { const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; const accessToken = generateToken(payload, TOKEN_TYPES.CUSTOMER); expect(() => { verifyRefreshToken(accessToken, TOKEN_TYPES.CUSTOMER); }).toThrow('Invalid refresh token'); }); it('should reject refresh token with wrong token type', () => { const payload = { user: { userId: 1, email: 'admin@example.com', fullName: 'Admin User' } }; const adminRefreshToken = generateRefreshToken( payload, TOKEN_TYPES.ADMIN ); expect(() => { verifyRefreshToken(adminRefreshToken, TOKEN_TYPES.CUSTOMER); }).toThrow('Invalid refresh token'); }); it('should reject expired refresh token', (done) => { // Temporarily set a short expiry const originalExpiry = process.env.JWT_CUSTOMER_REFRESH_TOKEN_EXPIRY; process.env.JWT_CUSTOMER_REFRESH_TOKEN_EXPIRY = '1'; const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; const token = generateRefreshToken(payload, TOKEN_TYPES.CUSTOMER); // Wait for token to expire setTimeout(() => { expect(() => { verifyRefreshToken(token, TOKEN_TYPES.CUSTOMER); }).toThrow('Refresh token has expired'); process.env.JWT_CUSTOMER_REFRESH_TOKEN_EXPIRY = originalExpiry; done(); }, 1500); }, 3000); it('should throw error if refresh secret is not configured', () => { const originalSecret = process.env.JWT_CUSTOMER_REFRESH_SECRET; delete process.env.JWT_CUSTOMER_REFRESH_SECRET; const token = 'some.jwt.token'; expect(() => { verifyRefreshToken(token, TOKEN_TYPES.CUSTOMER); }).toThrow('JWT refresh secret for customer is not configured'); process.env.JWT_CUSTOMER_REFRESH_SECRET = originalSecret; }); }); describe('decodeToken', () => { it('should decode token without verification', () => { const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; const token = generateToken(payload, TOKEN_TYPES.CUSTOMER); const decoded = decodeToken(token); expect(decoded).toBeDefined(); expect(decoded.customer.customerId).toBe(1); expect(decoded.customer.email).toBe('customer@example.com'); expect(decoded.tokenType).toBe(TOKEN_TYPES.CUSTOMER); expect(decoded.tokenKind).toBe('access'); }); it('should decode expired token without throwing', () => { const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; const token = generateToken(payload, TOKEN_TYPES.CUSTOMER, 1); // Decode immediately should work const decoded = decodeToken(token); expect(decoded).toBeDefined(); expect(decoded.customer.customerId).toBe(1); }); it('should return null for invalid token', () => { const decoded = decodeToken('invalid.token'); expect(decoded).toBeNull(); }); }); describe('Token Security', () => { it('should use different secrets for access and refresh tokens', () => { const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; const accessToken = generateToken(payload, TOKEN_TYPES.CUSTOMER); const refreshToken = generateRefreshToken(payload, TOKEN_TYPES.CUSTOMER); // Access token should fail when verified with refresh secret expect(() => { verifyRefreshToken(accessToken, TOKEN_TYPES.CUSTOMER); }).toThrow(); // Refresh token should fail when verified with access secret expect(() => { verifyToken(refreshToken, TOKEN_TYPES.CUSTOMER); }).toThrow(); }); it('should prevent token type confusion between admin and customer', () => { const adminPayload = { user: { userId: 1, email: 'admin@example.com', fullName: 'Admin User' } }; const customerPayload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; const adminToken = generateToken(adminPayload, TOKEN_TYPES.ADMIN); const customerToken = generateToken( customerPayload, TOKEN_TYPES.CUSTOMER ); // Admin token should not verify as customer token expect(() => { verifyToken(adminToken, TOKEN_TYPES.CUSTOMER); }).toThrow(); // Customer token should not verify as admin token expect(() => { verifyToken(customerToken, TOKEN_TYPES.ADMIN); }).toThrow(); }); it('should include audience and issuer in token', () => { const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; const token = generateToken(payload, TOKEN_TYPES.CUSTOMER); const decoded = decodeToken(token); expect(decoded.aud).toBe(TOKEN_TYPES.CUSTOMER); expect(decoded.iss).toBe('evershop-test'); }); }); describe('Integration Tests', () => { it('should support full token refresh flow', () => { // 1. Generate initial tokens const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; const accessToken = generateToken(payload, TOKEN_TYPES.CUSTOMER); const refreshToken = generateRefreshToken(payload, TOKEN_TYPES.CUSTOMER); // 2. Verify access token const decodedAccess = verifyToken(accessToken, TOKEN_TYPES.CUSTOMER); expect(decodedAccess.customer.customerId).toBe(1); // 3. Verify refresh token const decodedRefresh = verifyRefreshToken( refreshToken, TOKEN_TYPES.CUSTOMER ); expect(decodedRefresh.customer.customerId).toBe(1); // 4. Generate new access token using refresh token data const newAccessToken = generateToken( { customer: decodedRefresh.customer }, TOKEN_TYPES.CUSTOMER ); const decodedNewAccess = verifyToken( newAccessToken, TOKEN_TYPES.CUSTOMER ); expect(decodedNewAccess.customer.customerId).toBe(1); }); it('should handle admin login/refresh flow', () => { const payload = { user: { userId: 123, email: 'admin@example.com', fullName: 'Admin User' } }; // Login - generate both tokens const accessToken = generateToken(payload, TOKEN_TYPES.ADMIN); const refreshToken = generateRefreshToken(payload, TOKEN_TYPES.ADMIN); // Verify access token works const verifiedAccess = verifyToken(accessToken, TOKEN_TYPES.ADMIN); expect(verifiedAccess.user.userId).toBe(123); // Use refresh token to get new access token const verifiedRefresh = verifyRefreshToken( refreshToken, TOKEN_TYPES.ADMIN ); const newAccessToken = generateToken( { user: verifiedRefresh.user }, TOKEN_TYPES.ADMIN ); // Verify new access token const verifiedNewAccess = verifyToken(newAccessToken, TOKEN_TYPES.ADMIN); expect(verifiedNewAccess.user.userId).toBe(123); }); }); describe('Environment Configuration Tests', () => { let originalEnv; beforeAll(() => { // Save original environment variables originalEnv = { JWT_ADMIN_SECRET: process.env.JWT_ADMIN_SECRET, JWT_ADMIN_REFRESH_SECRET: process.env.JWT_ADMIN_REFRESH_SECRET, JWT_CUSTOMER_SECRET: process.env.JWT_CUSTOMER_SECRET, JWT_CUSTOMER_REFRESH_SECRET: process.env.JWT_CUSTOMER_REFRESH_SECRET }; }); afterAll(() => { // Restore original environment variables process.env.JWT_ADMIN_SECRET = originalEnv.JWT_ADMIN_SECRET; process.env.JWT_ADMIN_REFRESH_SECRET = originalEnv.JWT_ADMIN_REFRESH_SECRET; process.env.JWT_CUSTOMER_SECRET = originalEnv.JWT_CUSTOMER_SECRET; process.env.JWT_CUSTOMER_REFRESH_SECRET = originalEnv.JWT_CUSTOMER_REFRESH_SECRET; }); describe('Missing Access Token Secrets', () => { it('should throw error when admin access token secret is missing', () => { delete process.env.JWT_ADMIN_SECRET; const payload = { user: { userId: 1, email: 'admin@example.com', fullName: 'Admin User' } }; expect(() => { generateToken(payload, TOKEN_TYPES.ADMIN); }).toThrow('JWT secret for admin is not configured'); // Restore for other tests process.env.JWT_ADMIN_SECRET = originalEnv.JWT_ADMIN_SECRET; }); it('should throw error when customer access token secret is missing', () => { delete process.env.JWT_CUSTOMER_SECRET; const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; expect(() => { generateToken(payload, TOKEN_TYPES.CUSTOMER); }).toThrow('JWT secret for customer is not configured'); // Restore for other tests process.env.JWT_CUSTOMER_SECRET = originalEnv.JWT_CUSTOMER_SECRET; }); it('should throw error when verifying token with missing admin secret', () => { // First generate a token with valid secret const payload = { user: { userId: 1, email: 'admin@example.com', fullName: 'Admin User' } }; const token = generateToken(payload, TOKEN_TYPES.ADMIN); // Now delete the secret delete process.env.JWT_ADMIN_SECRET; expect(() => { verifyToken(token, TOKEN_TYPES.ADMIN); }).toThrow('JWT secret for admin is not configured'); // Restore for other tests process.env.JWT_ADMIN_SECRET = originalEnv.JWT_ADMIN_SECRET; }); it('should throw error when verifying token with missing customer secret', () => { // First generate a token with valid secret const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; const token = generateToken(payload, TOKEN_TYPES.CUSTOMER); // Now delete the secret delete process.env.JWT_CUSTOMER_SECRET; expect(() => { verifyToken(token, TOKEN_TYPES.CUSTOMER); }).toThrow('JWT secret for customer is not configured'); // Restore for other tests process.env.JWT_CUSTOMER_SECRET = originalEnv.JWT_CUSTOMER_SECRET; }); }); describe('Missing Refresh Token Secrets', () => { it('should throw error when admin refresh token secret is missing', () => { delete process.env.JWT_ADMIN_REFRESH_SECRET; const payload = { user: { userId: 1, email: 'admin@example.com', fullName: 'Admin User' } }; expect(() => { generateRefreshToken(payload, TOKEN_TYPES.ADMIN); }).toThrow('JWT refresh secret for admin is not configured'); // Restore for other tests process.env.JWT_ADMIN_REFRESH_SECRET = originalEnv.JWT_ADMIN_REFRESH_SECRET; }); it('should throw error when customer refresh token secret is missing', () => { delete process.env.JWT_CUSTOMER_REFRESH_SECRET; const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; expect(() => { generateRefreshToken(payload, TOKEN_TYPES.CUSTOMER); }).toThrow('JWT refresh secret for customer is not configured'); // Restore for other tests process.env.JWT_CUSTOMER_REFRESH_SECRET = originalEnv.JWT_CUSTOMER_REFRESH_SECRET; }); it('should throw error when verifying refresh token with missing admin refresh secret', () => { // First generate a refresh token with valid secret const payload = { user: { userId: 1, email: 'admin@example.com', fullName: 'Admin User' } }; const token = generateRefreshToken(payload, TOKEN_TYPES.ADMIN); // Now delete the refresh secret delete process.env.JWT_ADMIN_REFRESH_SECRET; expect(() => { verifyRefreshToken(token, TOKEN_TYPES.ADMIN); }).toThrow('JWT refresh secret for admin is not configured'); // Restore for other tests process.env.JWT_ADMIN_REFRESH_SECRET = originalEnv.JWT_ADMIN_REFRESH_SECRET; }); it('should throw error when verifying refresh token with missing customer refresh secret', () => { // First generate a refresh token with valid secret const payload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer User', groupId: 1 } }; const token = generateRefreshToken(payload, TOKEN_TYPES.CUSTOMER); // Now delete the refresh secret delete process.env.JWT_CUSTOMER_REFRESH_SECRET; expect(() => { verifyRefreshToken(token, TOKEN_TYPES.CUSTOMER); }).toThrow('JWT refresh secret for customer is not configured'); // Restore for other tests process.env.JWT_CUSTOMER_REFRESH_SECRET = originalEnv.JWT_CUSTOMER_REFRESH_SECRET; }); }); describe('Missing All Secrets', () => { it('should fail gracefully when all secrets are missing', () => { // Remove all secrets delete process.env.JWT_ADMIN_SECRET; delete process.env.JWT_ADMIN_REFRESH_SECRET; delete process.env.JWT_CUSTOMER_SECRET; delete process.env.JWT_CUSTOMER_REFRESH_SECRET; const adminPayload = { user: { userId: 1, email: 'admin@example.com', fullName: 'Admin' } }; const customerPayload = { customer: { customerId: 1, email: 'customer@example.com', fullName: 'Customer', groupId: 1 } }; // All token generation should fail expect(() => generateToken(adminPayload, TOKEN_TYPES.ADMIN)).toThrow(); expect(() => generateToken(customerPayload, TOKEN_TYPES.CUSTOMER) ).toThrow(); expect(() => generateRefreshToken(adminPayload, TOKEN_TYPES.ADMIN) ).toThrow(); expect(() => generateRefreshToken(customerPayload, TOKEN_TYPES.CUSTOMER) ).toThrow(); // Restore all secrets process.env.JWT_ADMIN_SECRET = originalEnv.JWT_ADMIN_SECRET; process.env.JWT_ADMIN_REFRESH_SECRET = originalEnv.JWT_ADMIN_REFRESH_SECRET; process.env.JWT_CUSTOMER_SECRET = originalEnv.JWT_CUSTOMER_SECRET; process.env.JWT_CUSTOMER_REFRESH_SECRET = originalEnv.JWT_CUSTOMER_REFRESH_SECRET; }); }); }); }); ================================================ FILE: packages/evershop/src/lib/util/tests/unit/util.merge.test.js ================================================ import { merge } from '../../merge.js'; describe('Test until merge', () => { it('It should thrown an exception if `object` is not an object or null', () => { const a = 1; const b = { b: 1 }; expect(() => merge(a, b)).toThrow(Error); }); it('It should thrown an exception if `object` is not an object or null', () => { const a = { a: 1 }; const b = null; expect(() => merge(a, b)).toThrow(Error); }); it('It should return an object contains all property from 2 provided object', () => { const a = { a: 1 }; const b = { b: 1, c: 1 }; expect(merge(a, b)).toEqual({ a: 1, b: 1, c: 1 }); }); it('It should not overwrite the value from the first object if it is existed', () => { const a = { a: 1 }; const b = { a: 2, c: 1 }; const c = merge(a, b); expect(c.a).toEqual(2); }); it('It should overwrite the value from the first object', () => { const a = { a: '', c: null, d: [] }; const b = { a: 2, c: 1, d: 1 }; const c = merge(a, b); expect(c.a).toEqual(2); expect(c.c).toEqual(1); expect(c.d).toEqual(1); }); it('It should merge array property from 2 objects', () => { const a = { a: [1, 2] }; const b = { a: [2, 3], c: 1 }; const c = merge(a, b); expect(c.a).toEqual([1, 2, 3]); }); it('It should thrown an exception if the maximum depth is exceeded', () => { const a = { a: { b: { c: { d: { e: { f: { g: { h: { i: { j: { k: 1 } } } } } } } } } } }; const b = { a: { b: { c: { d: { e: { f: { g: { h: { i: { j: { k: 2 } } } } } } } } } } }; expect(() => merge(a, b, 5)).toThrow(Error); }); }); ================================================ FILE: packages/evershop/src/lib/util/tests/unit/util.parseImageSizes.test.js ================================================ import { parseImageSizes, evaluateCondition, evaluateMediaQuery, convertToPixels } from '../../parseImageSizes.js'; describe('parseImageSizes', () => { describe('parseImageSizes function', () => { test('should handle fixed pixel values', () => { const result = parseImageSizes('500px'); expect(result).toEqual([320, 640, 750, 828]); // Based on actual implementation: sizes >= 500*0.5, slice(0,4) expect(result.length).toBe(4); }); test('should handle simple 100vw value', () => { const result = parseImageSizes('100vw'); expect(result).toEqual([ 320, 640, 750, 828, 1080, 1200, 1920, 2048, 3840 ]); expect(result.length).toBe(9); }); test('should handle complex media queries with multiple conditions', () => { const sizes = '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw'; const result = parseImageSizes(sizes); expect(Array.isArray(result)).toBe(true); expect(result.length).toBeGreaterThanOrEqual(3); expect(result.every((size) => typeof size === 'number')).toBe(true); }); test('should handle single condition with viewport width', () => { const sizes = '(max-width: 750px) 100vw, 50vw'; const result = parseImageSizes(sizes); expect(result).toContain(320); expect(result).toContain(640); expect(result).toContain(750); }); test('should ensure minimum variety of sizes', () => { const result = parseImageSizes('300px'); expect(result.length).toBeGreaterThanOrEqual(3); }); test('should remove duplicates and sort results', () => { const result = parseImageSizes('100vw'); const sorted = [...result].sort((a, b) => a - b); expect(result).toEqual(sorted); expect(new Set(result).size).toBe(result.length); }); test('should handle empty or invalid sizes', () => { expect(() => parseImageSizes('')).toThrow( 'Invalid sizes attribute: must be a non-empty string' ); expect(() => parseImageSizes(' ')).toThrow( 'Invalid sizes attribute: cannot be empty or whitespace only' ); expect(() => parseImageSizes(null)).toThrow( 'Invalid sizes attribute: must be a non-empty string' ); expect(() => parseImageSizes(undefined)).toThrow( 'Invalid sizes attribute: must be a non-empty string' ); expect(() => parseImageSizes(123)).toThrow( 'Invalid sizes attribute: must be a non-empty string' ); }); test('should throw for invalid pixel values', () => { expect(() => parseImageSizes('0px')).toThrow( 'Invalid pixel value in sizes attribute: "0px" must be a positive number followed by "px"' ); expect(() => parseImageSizes('-100px')).toThrow( 'Invalid pixel value in sizes attribute: "-100px" must be a positive number followed by "px"' ); expect(() => parseImageSizes('abcpx')).toThrow( 'Invalid pixel value in sizes attribute: "abcpx" must be a positive number followed by "px"' ); }); test('should throw for invalid conditions', () => { expect(() => parseImageSizes('invalid')).toThrow( 'Invalid condition in sizes attribute: "invalid" - must contain a valid CSS length value or media query' ); expect(() => parseImageSizes('100invalid')).toThrow( 'Invalid condition in sizes attribute: "100invalid" - must contain a valid CSS length value or media query' ); expect(() => parseImageSizes('(max-width: 640px 100vw')).toThrow( 'Invalid condition in sizes attribute: "(max-width: 640px 100vw" - must contain a valid CSS length value or media query' ); }); }); describe('evaluateCondition function', () => { test('should handle condition with media query', () => { const condition = '(max-width: 640px) 100vw'; const result = evaluateCondition(condition, 500); expect(result).toBe(500); // 100vw at 500px device = 500px }); test('should handle condition without media query', () => { const condition = '50vw'; const result = evaluateCondition(condition, 1000); expect(result).toBe(500); // 50vw at 1000px device = 500px }); test('should handle fixed pixel values', () => { const condition = '300px'; const result = evaluateCondition(condition, 1000); expect(result).toBe(300); }); test('should return null for non-matching media query', () => { const condition = '(max-width: 500px) 100vw'; const result = evaluateCondition(condition, 800); expect(result).toBeNull(); }); test('should handle em units', () => { const condition = '20em'; const result = evaluateCondition(condition, 1000); expect(result).toBe(320); // 20em * 16px = 320px }); test('should handle rem units', () => { const condition = '25rem'; const result = evaluateCondition(condition, 1000); expect(result).toBe(400); // 25rem * 16px = 400px }); test('should handle ch units', () => { const condition = '50ch'; const result = evaluateCondition(condition, 1000); expect(result).toBe(400); // 50ch * 8px = 400px }); test('should handle vw units', () => { const condition = '75vw'; const result = evaluateCondition(condition, 800); expect(result).toBe(600); // 75% of 800px = 600px }); test('should handle vh units', () => { const condition = '50vh'; const result = evaluateCondition(condition, 1000); // For 1000px device, assumed height is 1000 * 0.6 = 600px (desktop), so 50vh = 300px expect(result).toBe(300); }); test('should trim whitespace', () => { const condition = ' 50vw '; const result = evaluateCondition(condition, 1000); expect(result).toBe(500); }); }); describe('evaluateMediaQuery function', () => { test('should handle max-width queries', () => { expect(evaluateMediaQuery('max-width: 640px', 500)).toBe(true); expect(evaluateMediaQuery('max-width: 640px', 640)).toBe(true); expect(evaluateMediaQuery('max-width: 640px', 800)).toBe(false); }); test('should handle min-width queries', () => { expect(evaluateMediaQuery('min-width: 640px', 800)).toBe(true); expect(evaluateMediaQuery('min-width: 640px', 640)).toBe(true); expect(evaluateMediaQuery('min-width: 640px', 500)).toBe(false); }); test('should handle max-device-width queries', () => { expect(evaluateMediaQuery('max-device-width: 750px', 600)).toBe(true); expect(evaluateMediaQuery('max-device-width: 750px', 750)).toBe(true); expect(evaluateMediaQuery('max-device-width: 750px', 800)).toBe(false); }); test('should handle min-device-width queries', () => { expect(evaluateMediaQuery('min-device-width: 750px', 1000)).toBe(true); expect(evaluateMediaQuery('min-device-width: 750px', 750)).toBe(true); expect(evaluateMediaQuery('min-device-width: 750px', 600)).toBe(false); }); test('should handle decimal values in media queries', () => { expect(evaluateMediaQuery('max-width: 749.5px', 749)).toBe(true); expect(evaluateMediaQuery('max-width: 749.5px', 750)).toBe(false); }); test('should handle orientation landscape', () => { // Assuming landscape is when width >= height (simplified) expect(evaluateMediaQuery('orientation: landscape', 1920)).toBe(true); }); test('should handle orientation portrait', () => { // Assuming portrait is when width < height (simplified) expect(evaluateMediaQuery('orientation: portrait', 375)).toBe(true); }); test('should handle aspect-ratio queries', () => { // Basic aspect-ratio test const result = evaluateMediaQuery('aspect-ratio: 16/9', 1920); expect(typeof result).toBe('boolean'); }); test('should handle whitespace in queries', () => { expect(evaluateMediaQuery(' max-width : 640px ', 500)).toBe(true); }); test('should return true for unsupported queries', () => { const result = evaluateMediaQuery('unsupported-feature: value', 1000); expect(result).toBe(true); // Implementation returns true for unknown queries }); }); describe('convertToPixels function', () => { test('should convert px values', () => { expect(convertToPixels(100, 'px', 1000)).toBe(100); }); test('should convert em values', () => { expect(convertToPixels(2, 'em', 1000)).toBe(32); // 2em * 16px = 32px }); test('should convert rem values', () => { expect(convertToPixels(1.5, 'rem', 1000)).toBe(24); // 1.5rem * 16px = 24px }); test('should convert ch values', () => { expect(convertToPixels(10, 'ch', 1000)).toBe(80); // 10ch * 8px = 80px }); test('should convert vw values', () => { expect(convertToPixels(50, 'vw', 1000)).toBe(500); // 50vw of 1000px = 500px }); test('should convert vh values', () => { expect(convertToPixels(25, 'vh', 800)).toBe(120); // For 800px: assumedHeight = 800*1.5=1200, 25vh = 300, but for desktop (>750) it's 800*0.6=480, so 25vh=120 }); test('should convert vmin values', () => { expect(convertToPixels(50, 'vmin', 1000)).toBe(300); // For 1000px desktop: min(1000, 600) = 600, so 50vmin = 300px }); test('should convert vmax values', () => { expect(convertToPixels(50, 'vmax', 1000)).toBe(500); // For 1000px desktop: max(1000, 600) = 1000, so 50vmax = 500px }); test('should convert percentage values', () => { expect(convertToPixels(75, '%', 800)).toBe(600); // 75% of 800px = 600px }); test('should handle edge cases', () => { expect(convertToPixels(0, 'px', 1000)).toBe(0); expect(convertToPixels(100, 'unknown', 1000)).toBe(100); // fallback }); test('should handle decimal values', () => { expect(convertToPixels(1.5, 'em', 1000)).toBe(24); expect(convertToPixels(33.33, 'vw', 900)).toBe(300); // ~33.33vw of 900px }); }); describe('integration tests', () => { test('should handle realistic mobile-first responsive sizes', () => { const sizes = '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw'; const result = parseImageSizes(sizes); expect(result).toContain(320); // Mobile full width expect(result).toContain(640); // Mobile breakpoint // More flexible checks based on actual implementation expect(result.some((size) => size >= 300 && size <= 700)).toBe(true); // Some tablet/desktop sizes expect(result.some((size) => size >= 200 && size <= 500)).toBe(true); // Some smaller sizes }); test('should handle complex breakpoints with em units', () => { const sizes = '(max-width: 40em) 100vw, (max-width: 64em) 50vw, 25vw'; const result = parseImageSizes(sizes); expect(Array.isArray(result)).toBe(true); expect(result.length).toBeGreaterThanOrEqual(3); }); test('should maintain performance with many conditions', () => { const sizes = '(max-width: 320px) 100vw, (max-width: 640px) 90vw, (max-width: 750px) 80vw, (max-width: 1080px) 70vw, (max-width: 1200px) 60vw, 50vw'; const startTime = performance.now(); const result = parseImageSizes(sizes); const endTime = performance.now(); expect(endTime - startTime).toBeLessThan(10); // Should complete in less than 10ms expect(Array.isArray(result)).toBe(true); }); }); describe('comprehensive CSS units coverage', () => { describe('absolute length units', () => { test('should handle px units', () => { expect(convertToPixels(100, 'px', 1000)).toBe(100); }); test('should handle pt units (points)', () => { // Note: Implementation should support pt (1pt = 1.33px) const result = convertToPixels(12, 'pt', 1000); expect(typeof result).toBe('number'); }); test('should handle pc units (picas)', () => { // Note: Implementation should support pc (1pc = 16px) const result = convertToPixels(1, 'pc', 1000); expect(typeof result).toBe('number'); }); test('should handle in units (inches)', () => { // Note: Implementation should support in (1in = 96px) const result = convertToPixels(1, 'in', 1000); expect(typeof result).toBe('number'); }); test('should handle cm units (centimeters)', () => { // Note: Implementation should support cm (1cm = 37.8px) const result = convertToPixels(1, 'cm', 1000); expect(typeof result).toBe('number'); }); test('should handle mm units (millimeters)', () => { // Note: Implementation should support mm (1mm = 3.78px) const result = convertToPixels(10, 'mm', 1000); expect(typeof result).toBe('number'); }); }); describe('relative length units', () => { test('should handle em units', () => { expect(convertToPixels(2, 'em', 1000)).toBe(32); // 2em * 16px = 32px }); test('should handle rem units', () => { expect(convertToPixels(1.5, 'rem', 1000)).toBe(24); // 1.5rem * 16px = 24px }); test('should handle ex units', () => { // Note: Implementation should support ex (x-height, typically ~0.5em) const result = convertToPixels(2, 'ex', 1000); expect(typeof result).toBe('number'); }); test('should handle ch units', () => { expect(convertToPixels(10, 'ch', 1000)).toBe(80); // 10ch * 8px = 80px }); test('should handle ic units', () => { // Note: Implementation should support ic (ideographic character width) const result = convertToPixels(5, 'ic', 1000); expect(typeof result).toBe('number'); }); test('should handle lh units', () => { // Note: Implementation should support lh (line height) const result = convertToPixels(2, 'lh', 1000); expect(typeof result).toBe('number'); }); }); describe('viewport units', () => { test('should handle vw units', () => { expect(convertToPixels(50, 'vw', 1000)).toBe(500); // 50vw of 1000px = 500px }); test('should handle vh units', () => { expect(convertToPixels(25, 'vh', 800)).toBe(120); // Based on implementation logic }); test('should handle vmin units', () => { expect(convertToPixels(50, 'vmin', 1000)).toBe(300); // Based on implementation logic }); test('should handle vmax units', () => { expect(convertToPixels(50, 'vmax', 1000)).toBe(500); // Based on implementation logic }); test('should handle vi units (inline viewport)', () => { // Note: Implementation should support vi const result = convertToPixels(50, 'vi', 1000); expect(typeof result).toBe('number'); }); test('should handle vb units (block viewport)', () => { // Note: Implementation should support vb const result = convertToPixels(50, 'vb', 1000); expect(typeof result).toBe('number'); }); }); describe('container query units', () => { test('should handle cqw units', () => { // Note: Implementation should support cqw (container query width) const result = convertToPixels(50, 'cqw', 1000); expect(typeof result).toBe('number'); }); test('should handle cqh units', () => { // Note: Implementation should support cqh (container query height) const result = convertToPixels(50, 'cqh', 1000); expect(typeof result).toBe('number'); }); test('should handle cqi units', () => { // Note: Implementation should support cqi (container query inline) const result = convertToPixels(50, 'cqi', 1000); expect(typeof result).toBe('number'); }); test('should handle cqb units', () => { // Note: Implementation should support cqb (container query block) const result = convertToPixels(50, 'cqb', 1000); expect(typeof result).toBe('number'); }); test('should handle cqmin units', () => { // Note: Implementation should support cqmin const result = convertToPixels(50, 'cqmin', 1000); expect(typeof result).toBe('number'); }); test('should handle cqmax units', () => { // Note: Implementation should support cqmax const result = convertToPixels(50, 'cqmax', 1000); expect(typeof result).toBe('number'); }); }); }); describe('complex media query combinations', () => { test('should handle multiple media features', () => { const sizes = '(min-width: 750px) and (max-width: 1080px) 50vw, 100vw'; const result = parseImageSizes(sizes); expect(Array.isArray(result)).toBe(true); }); test('should handle orientation-based queries', () => { const sizes = '(orientation: landscape) 50vw, (orientation: portrait) 100vw'; const result = parseImageSizes(sizes); expect(Array.isArray(result)).toBe(true); }); test('should handle aspect-ratio queries', () => { const sizes = '(min-aspect-ratio: 16/9) 33vw, 50vw'; const result = parseImageSizes(sizes); expect(Array.isArray(result)).toBe(true); }); test('should handle resolution queries', () => { const sizes = '(min-resolution: 2dppx) 50vw, 100vw'; const result = parseImageSizes(sizes); expect(Array.isArray(result)).toBe(true); }); test('should handle hover capability queries', () => { const sizes = '(hover: hover) 25vw, 50vw'; const result = parseImageSizes(sizes); expect(Array.isArray(result)).toBe(true); }); test('should handle pointer queries', () => { const sizes = '(pointer: fine) 33vw, 100vw'; const result = parseImageSizes(sizes); expect(Array.isArray(result)).toBe(true); }); test('should handle prefers-color-scheme queries', () => { const sizes = '(prefers-color-scheme: dark) 40vw, 50vw'; const result = parseImageSizes(sizes); expect(Array.isArray(result)).toBe(true); }); test('should handle prefers-reduced-motion queries', () => { const sizes = '(prefers-reduced-motion: reduce) 100vw, 50vw'; const result = parseImageSizes(sizes); expect(Array.isArray(result)).toBe(true); }); }); describe('edge cases and special values', () => { test('should handle calc() expressions', () => { const condition = 'calc(100vw - 2rem)'; // Note: Implementation should handle calc() expressions const result = evaluateCondition(condition, 1000); expect(typeof result === 'number' || result === null).toBe(true); }); test('should handle min() function', () => { const condition = 'min(100vw, 800px)'; // Note: Implementation should handle min() function const result = evaluateCondition(condition, 1000); expect(typeof result === 'number' || result === null).toBe(true); }); test('should handle max() function', () => { const condition = 'max(50vw, 400px)'; // Note: Implementation should handle max() function const result = evaluateCondition(condition, 1000); expect(typeof result === 'number' || result === null).toBe(true); }); test('should handle clamp() function', () => { const condition = 'clamp(200px, 50vw, 800px)'; // Note: Implementation should handle clamp() function const result = evaluateCondition(condition, 1000); expect(typeof result === 'number' || result === null).toBe(true); }); test('should handle zero values', () => { expect(convertToPixels(0, 'px', 1000)).toBe(0); expect(convertToPixels(0, 'vw', 1000)).toBe(0); expect(convertToPixels(0, 'rem', 1000)).toBe(0); }); test('should handle very large values', () => { expect(convertToPixels(9999, 'px', 1000)).toBe(9999); expect(convertToPixels(100, 'vw', 10000)).toBe(10000); }); test('should handle decimal values with many decimal places', () => { expect(convertToPixels(33.333333, 'vw', 900)).toBe(300); expect(convertToPixels(1.5625, 'rem', 1000)).toBe(25); }); test('should handle negative values gracefully', () => { expect(convertToPixels(-10, 'px', 1000)).toBe(-10); expect(convertToPixels(-5, 'vw', 1000)).toBe(-50); }); }); describe('real-world sizes attribute examples', () => { test('should handle typical responsive image sizes', () => { const sizes = '(max-width: 320px) 280px, (max-width: 640px) 600px, (max-width: 1024px) 960px, 1200px'; const result = parseImageSizes(sizes); expect(result).toContain(280); expect(result).toContain(600); expect(result).toContain(960); expect(result).toContain(1200); }); test('should handle Bootstrap-style breakpoints', () => { const sizes = '(max-width: 575.98px) 100vw, (max-width: 767.98px) 100vw, (max-width: 991.98px) 50vw, (max-width: 1199.98px) 33vw, 25vw'; const result = parseImageSizes(sizes); expect(Array.isArray(result)).toBe(true); expect(result.length).toBeGreaterThanOrEqual(4); }); test('should handle Tailwind CSS breakpoints', () => { const sizes = '(max-width: 640px) 100vw, (max-width: 750px) 100vw, (max-width: 1080px) 50vw, (max-width: 1200px) 33vw, 25vw'; const result = parseImageSizes(sizes); expect(Array.isArray(result)).toBe(true); expect(result.length).toBeGreaterThanOrEqual(4); }); test('should handle art direction scenarios', () => { const sizes = '(orientation: portrait) and (max-width: 480px) 100vw, (orientation: landscape) and (max-height: 480px) 100vh, 50vw'; const result = parseImageSizes(sizes); expect(Array.isArray(result)).toBe(true); }); test('should handle print media queries', () => { const sizes = 'print 100%, screen and (max-width: 600px) 100vw, 50vw'; const result = parseImageSizes(sizes); expect(Array.isArray(result)).toBe(true); }); }); describe('error handling and malformed input', () => { test('should handle missing closing parentheses', () => { expect(() => parseImageSizes('(max-width: 640px 100vw, 50vw')).toThrow( 'Invalid condition in sizes attribute: "(max-width: 640px 100vw" - must contain a valid CSS length value or media query' ); }); test('should handle missing opening parentheses', () => { // This should actually be valid as it's just "max-width: 640px) 100vw" which isn't a media query const sizes = 'max-width: 640px) 100vw, 50vw'; expect(() => parseImageSizes(sizes)).toThrow( 'Invalid condition in sizes attribute: "max-width: 640px) 100vw" - must contain a valid CSS length value or media query' ); }); test('should handle invalid CSS units', () => { expect(() => parseImageSizes('100invalid')).toThrow( 'Invalid condition in sizes attribute: "100invalid" - must contain a valid CSS length value or media query' ); }); test('should handle mixed valid and invalid conditions', () => { expect(() => parseImageSizes( '(max-width: 640px) 100invalidunit, (max-width: 1024px) 50vw, 25vw' ) ).toThrow( 'Invalid condition in sizes attribute: "(max-width: 640px) 100invalidunit" - must contain a valid CSS length value or media query' ); }); test('should handle extra commas', () => { // Extra commas should be filtered out, but valid conditions should still work const sizes = '(max-width: 640px) 100vw,, 50vw,'; const result = parseImageSizes(sizes); expect(Array.isArray(result)).toBe(true); }); test('should handle spaces in unexpected places', () => { // Should handle reasonable spacing variations const sizes = '( max-width : 640px ) 100 vw , 50 vw'; const result = parseImageSizes(sizes); expect(Array.isArray(result)).toBe(true); }); test('should throw for completely empty conditions after filtering', () => { expect(() => parseImageSizes(',,, ,,')).toThrow( 'Invalid sizes attribute: no valid conditions found after parsing' ); }); test('should throw for unmatched closing parenthesis without media query', () => { expect(() => parseImageSizes('100vw) something')).toThrow( 'Invalid condition in sizes attribute: "100vw) something" - must contain a valid CSS length value or media query' ); }); test('should throw for incomplete media queries', () => { expect(() => parseImageSizes('(max-width')).toThrow( 'Invalid condition in sizes attribute: "(max-width" - must contain a valid CSS length value or media query' ); }); }); }); ================================================ FILE: packages/evershop/src/lib/util/tests/unit/util.preloadScan.test.js ================================================ import { injectPreloadLinks, injectPreloadLinksAfterCharset, cleanupPreloadAttributes, processPreloadImages } from '../../preloadScan.js'; describe('preloadScan', () => { describe('injectPreloadLinks function', () => { test('should inject preload links before closing head tag', () => { const html = ` Test Test `; const result = injectPreloadLinks(html); expect(result).toContain( ']*>\s*<\/head>/); }); test('should handle multiple preload images', () => { const html = ` Test Image 1 Image 2 Normal `; const result = injectPreloadLinks(html); expect(result).toContain('href="/image1.jpg"'); expect(result).toContain('href="/image2.jpg"'); expect(result).not.toContain('href="/normal.jpg"'); expect((result.match(/ { const html = ` Test Normal `; const result = injectPreloadLinks(html); expect(result).toBe(html); }); test('should handle missing head tag gracefully', () => { const html = ` `; const result = injectPreloadLinks(html); expect(result).toBe(html); }); test('should handle images with crossorigin attribute', () => { const html = ` `; const result = injectPreloadLinks(html); expect(result).toContain('crossorigin="anonymous"'); }); }); describe('injectPreloadLinksAfterCharset function', () => { test('should inject preload links after charset meta tag', () => { const html = ` Test `; const result = injectPreloadLinksAfterCharset(html); expect(result).toContain(''); expect(result).toMatch(/\s* { const html = ` Test `; const result = injectPreloadLinksAfterCharset(html); expect(result).toContain(']*>\s*<\/head>/); }); test('should handle charset with different quote styles', () => { const html = ` Test `; const result = injectPreloadLinksAfterCharset(html); expect(result).toContain(' { test('should remove itemProp="preload" attributes', () => { const html = `Test Normal `; const result = cleanupPreloadAttributes(html); expect(result).not.toContain('itemProp="preload"'); expect(result).toContain('src="/test.jpg"'); expect(result).toContain('alt="Test"'); expect(result).toContain('src="/normal.jpg"'); expect(result).toContain('sizes="100vw"'); }); test('should handle single quotes', () => { const html = `Test`; const result = cleanupPreloadAttributes(html); expect(result).not.toContain("itemProp='preload'"); expect(result).toContain('src="/test.jpg"'); }); test('should handle extra whitespace around attributes', () => { const html = `Test`; const result = cleanupPreloadAttributes(html); expect(result).not.toContain('itemProp="preload"'); expect(result).toContain('src="/test.jpg"'); }); test('should return unchanged HTML if no preload attributes found', () => { const html = `Test Normal`; const result = cleanupPreloadAttributes(html); expect(result).toBe(html); }); }); describe('processPreloadImages function (complete pipeline)', () => { test('should process complete pipeline: inject and cleanup', () => { const html = ` Test Hero Normal `; const result = processPreloadImages(html); // Should have preload link expect(result).toContain( ' { const html = ` Normal `; const result = processPreloadImages(html); // Should have two preload links expect((result.match(/ { const html = ` Test Normal 1 Normal 2 `; const result = processPreloadImages(html); expect(result).toBe(html); }); }); describe('edge cases and error handling', () => { test('should handle malformed img tags', () => { const html = ` Test `; const result = processPreloadImages(html); // Should handle valid tags and skip malformed ones gracefully expect(result).toContain(' { const html = ` No src `; const result = processPreloadImages(html); // Should only create preload link for image with src expect((result.match(/ { const result = processPreloadImages(''); expect(result).toBe(''); }); test('should handle HTML with no body', () => { const html = ` Test `; const result = processPreloadImages(html); expect(result).toBe(html); }); test('should handle images with special characters in attributes', () => { const html = ` `; const result = processPreloadImages(html); expect(result).toContain('href="/test image.jpg"'); expect(result).toContain('imagesrcset="/test%20image.jpg 500w"'); expect(result).toContain('imagesizes="(max-width: 600px) 100vw, 50vw"'); }); test('should handle case-insensitive itemProp matching', () => { const html = ` `; const result = processPreloadImages(html); // Should match all variations expect((result.match(/ { const html = ` `; const result = processPreloadImages(html); expect(result).toContain(' { test('should correctly extract all preload-relevant attributes', () => { const html = ` Complex Image `; const result = processPreloadImages(html); expect(result).toContain('href="/complex-image.jpg"'); expect(result).toContain( 'imagesrcset="/complex-image.jpg 320w, /complex-image.jpg 640w, /complex-image.jpg 1024w"' ); expect(result).toContain( 'imagesizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"' ); expect(result).toContain('crossorigin="use-credentials"'); expect(result).toContain('fetchpriority="high"'); // Should preserve non-preload attributes in original img tag expect(result).toContain('alt="Complex Image"'); expect(result).toContain('class="hero-image"'); expect(result).toContain('loading="eager"'); // Should clean up itemProp expect(result).not.toContain('itemProp="preload"'); }); test('should handle missing optional attributes gracefully', () => { const html = ` `; const result = processPreloadImages(html); expect(result).toContain( '' ); expect(result).not.toContain('imagesrcset'); expect(result).not.toContain('imagesizes'); expect(result).not.toContain('crossorigin'); }); }); describe('performance and scalability', () => { test('should handle large HTML documents efficiently', () => { // Create a large HTML document with many images const manyImages = Array.from( { length: 100 }, (_, i) => `Image ${i}` ).join('\n'); const html = ` ${manyImages} `; const startTime = performance.now(); const result = processPreloadImages(html); const endTime = performance.now(); // Should complete in reasonable time (less than 50ms for 100 images) expect(endTime - startTime).toBeLessThan(50); // Should find correct number of preload images (every 10th image) expect((result.match(/ { const html = `

    `; const result = processPreloadImages(html); expect(result).toContain(' { it('It should return the init value if no processor provided', async () => { const initValue = 1; const value = await getValue('test', initValue); expect(value).toEqual(initValue); }); it('It should add processor to the registry', () => { const callback = () => {}; addProcessor('test', callback); const processors = getProcessors('test'); expect(processors).toEqual([ { callback, priority: 10 } ]); }); it('It should throw error if priority is not a number', () => { const callback = () => {}; expect(() => addProcessor('test', callback, 'abc')).toThrow(Error); }); it('It should add processor to the registry with priority', () => { const negativeCallback = () => {}; const beforeCallback = () => {}; const afterCallback = () => {}; addProcessor('test2', negativeCallback, -5); addProcessor('test2', beforeCallback, 5); addProcessor('test2', afterCallback, 20); const processors = getProcessors('test2'); expect(JSON.stringify(processors)).toEqual( JSON.stringify([ { callback: negativeCallback, priority: -5 }, { callback: beforeCallback, priority: 5 }, { callback: afterCallback, priority: 20 } ]) ); }); it('It should throw error if callback is not a function', () => { expect(() => addProcessor('test', 'abc')).toThrow(Error); }); it('It should accept async function as callback', () => { const callback = async () => {}; addProcessor('testasync', callback); const processors = getProcessors('testasync'); expect(processors).toEqual([ { callback, priority: 10 } ]); }); it('It should execute the processor function in order', async () => { const callback1 = jest.fn(async () => { return 1; }); const callback2 = jest.fn(async () => { return 2; }); const callback3 = jest.fn(async () => { return 3; }); addProcessor('test3', callback1, 10); addProcessor('test3', callback2, 5); addProcessor('test3', callback3, 20); const value = await getValue('test3', 1); expect(value).toEqual(3); expect(callback3).toHaveBeenCalled(); expect(callback2).toHaveBeenCalled(); expect(callback1).toHaveBeenCalled(); }); it('It should skip the processors if the init value and the context are identical', async () => { const callback1 = jest.fn(async () => { return 1; }); const callback2 = jest.fn(async () => { return 2; }); const callback3 = jest.fn(async () => { return 3; }); addProcessor('test4', callback1, 10); addProcessor('test4', callback2, 5); addProcessor('test4', callback3, 20); const value = await getValue('test4', 1, { a: 1 }); expect(value).toEqual(3); expect(callback3).toHaveBeenCalled(); expect(callback2).toHaveBeenCalled(); expect(callback1).toHaveBeenCalled(); const value2 = await getValue('test4', 1, { a: 1 }); expect(value2).toEqual(3); expect(callback3).toHaveBeenCalledTimes(1); expect(callback2).toHaveBeenCalledTimes(1); expect(callback1).toHaveBeenCalledTimes(1); }); it('It should overwrite the init value and the context if the init value and the context are not identical', async () => { const callback = jest.fn(async (value) => { return ++value; }); addProcessor('test5', callback, 10); const value = await getValue('test5', 1, { a: 1 }); expect(value).toEqual(2); expect(callback).toHaveBeenCalled(); const valueAgain = await getValue('test5', 1, { a: 1 }); expect(valueAgain).toEqual(2); expect(callback).toHaveBeenCalledTimes(1); const value2 = await getValue('test5', 2, { a: 2 }); expect(value2).toEqual(3); expect(callback).toHaveBeenCalledTimes(2); const value3 = await getValue('test5', 1, { a: 1 }); expect(value3).toEqual(2); expect(callback).toHaveBeenCalledTimes(3); }); it('It should throw an error if the value does not pass the validator', async () => { const callback = jest.fn(async (value) => { return ++value; }); addProcessor('test6', callback, 10); expect(async () => { await getValue('test6', 1, {}, (value) => { return value > 3; }); }).rejects.toThrow(Error); }); it('The getValueSync function should throw if the processor is async', () => { const callback = async () => {}; addProcessor('test7', callback); expect(() => { getValueSync('test7', 1); }).toThrow(Error); }); it('It should throw an error if one of the processor throws an error', async () => { const callback1 = jest.fn(async () => { return 1; }); const callback2 = jest.fn(async () => { throw new Error('error'); }); const callback3 = jest.fn(async () => { return 3; }); addProcessor('test8', callback1, 2); addProcessor('test8', callback2, 5); addProcessor('test8', callback3, 20); expect(async () => { await getValue('test8', 1); }).rejects.toThrow(Error); expect(callback3).not.toHaveBeenCalled(); expect(callback1).toHaveBeenCalled(); }); it('It should throw an error if one of the processor throws an error', () => { const callback1 = jest.fn(() => { return 1; }); const callback2 = jest.fn(() => { throw new Error('error'); }); const callback3 = jest.fn(() => { return 3; }); addProcessor('test9', callback1, 2); addProcessor('test9', callback2, 5); addProcessor('test9', callback3, 20); expect(() => { getValueSync('test9', 1); }).toThrow(Error); expect(callback3).not.toHaveBeenCalled(); expect(callback1).toHaveBeenCalled(); }); }); ================================================ FILE: packages/evershop/src/lib/util/validateConfiguration.js ================================================ import { getAjv } from '../../modules/base/services/getAjv.js'; import { getValueSync } from './registry.js'; export function validateConfiguration(config) { const ajv = getAjv(); const configSchema = getValueSync( 'configurationSchema', { type: 'object' }, null, (schema) => { ajv.validateSchema(schema); if (ajv.errors) { throw new Error( `Your configuration schema is not valid: ${ajv.errors[0].instancePath}` ); } else { return true; } } ); // Validate by using Ajv const validate = ajv.compile(configSchema); const reservedKeys = [ 'get', 'has', 'util', 'getConfigSources', 'makeHidden', 'makeImmutable', 'setModuleDefaults', 'watch', '_attachProtoDeep', '_cloneDeep', '_diffDeep', '_equalsDeep', '_extendDeep', '_get', '_getCmdLineArg', '_initParam', '_isObject', '_loadFileConfigs', '_parseFile', '_stripComments', '_stripYamlComments' ]; const configuration = Object.keys(config).reduce((acc, key) => { if (configSchema.properties[key] || !reservedKeys.includes(key)) { acc[key] = config[key]; } return acc; }, {}); const valid = validate(configuration); if (!valid) { throw new Error(errorFormatter(validate.errors)); } else { return true; } } function errorFormatter(errors) { const messages = ['Invalid configuration:']; errors.forEach((error) => { if (error.keyword === 'errorMessage') { messages.push(`${error.message}. ${error.instancePath}`); } else if (error.keyword === 'additionalProperties') { messages.push( `${error.instancePath}/${error.params.additionalProperty} is not allowed.` ); } else if (error.keyword === 'enum') { messages.push( `${ error.instancePath } must be one of the following values: ${error.params.allowedValues.join( ', ' )}` ); } else { messages.push(`${error.instancePath} ${error.message}`); } }); return messages.join('\n'); } ================================================ FILE: packages/evershop/src/lib/util/validator.ts ================================================ export type Validator = { id: string; func: (input: T) => boolean | Promise; errorMessage: string; }; export class ValidatorManager { private validators = new Map>(); constructor(initial: Validator[] = []) { for (const v of initial) { this.add(v); } } add(validator: Validator) { this.validators.set(validator.id, validator); } async validate(input: T) { const results = await Promise.allSettled( Array.from(this.validators.values()).map(async (validator) => { try { const isValid = await validator.func(input); return isValid ? null : validator.errorMessage; } catch (err: any) { return `${validator.errorMessage} (exception occurred)`; } }) ); const errors = results .map((r) => r.status === 'fulfilled' ? r.value : 'Unknown validation error' ) .filter((msg): msg is string => !!msg); return { valid: errors.length === 0, errors }; } validateSync(input: T) { const errors: string[] = []; for (const validator of this.validators.values()) { try { const isValid = validator.func(input); if (isValid instanceof Promise) { throw new Error( 'Synchronous validation expected, but got async function' ); } if (!isValid) { errors.push(validator.errorMessage); } } catch (err: any) { errors.push(err.message || 'Unknown validation error'); } } return { valid: errors.length === 0, errors }; } getAllIds() { return Array.from(this.validators.keys()); } getValidator(id: string) { return this.validators.get(id); } remove(id: string) { this.validators.delete(id); } clear() { this.validators.clear(); } } ================================================ FILE: packages/evershop/src/lib/webpack/createBaseConfig.js ================================================ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { SwcMinifyWebpackPlugin } from 'swc-minify-webpack-plugin'; import { getEnabledExtensions } from '../../bin/extension/index.js'; import { getCoreModules } from '../../bin/lib/loadModules.js'; import { CONSTANTS } from '../helpers.js'; import { getEnabledTheme } from '../util/getEnabledTheme.js'; import isProductionMode from '../util/isProductionMode.js'; import { loadCsvTranslationFiles } from './loaders/loadTranslationFromCsv.js'; // Get the directory name of the current module const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); function isRealDirectorySync(path) { try { const stats = fs.lstatSync(path); if (stats.isSymbolicLink()) { return false; } return stats.isDirectory(); } catch (err) { if (err.code === 'ENOENT') { return false; } throw err; } } export function createBaseConfig(isServer) { const extenions = getEnabledExtensions(); const coreModules = getCoreModules(); const theme = getEnabledTheme(); const loaders = [ { test: /\.m?js$/, resolve: { fullySpecified: false } }, { test: /\.js$/, exclude: { and: [/node_modules/], not: [ /@evershop[\\/]evershop/, ...extenions.map((ext) => { const regex = new RegExp( ext.resolve.replace(/\\/g, '[\\\\\\]').replace(/\//g, '[\\\\/]') ); return regex; }) ] }, use: [ { loader: path.resolve( CONSTANTS.LIBPATH, 'webpack/loaders/LayoutLoader.js' ) }, { loader: path.resolve( CONSTANTS.LIBPATH, 'webpack/loaders/GraphqlLoader.js' ) }, { loader: path.resolve( CONSTANTS.LIBPATH, 'webpack/loaders/TranslationLoader.js' ), options: { getTranslateData: async () => { const result = await loadCsvTranslationFiles(); return result; } } } ] } ]; const output = isServer ? { path: CONSTANTS.BUILDPATH, publicPath: CONSTANTS.BUILDPATH, filename: isServer === true ? '[name]/server/index.js' : '[name]/client/index.js', pathinfo: false } : { path: CONSTANTS.BUILDPATH, publicPath: isProductionMode() ? '/assets/' : '/', pathinfo: false }; if (!isProductionMode()) { Object.assign(output, { chunkFilename: (pathData) => `${pathData.chunk.renderedHash}/client/${pathData.chunk.runtime}.js` }); } else { Object.assign(output, { chunkFilename: (pathData) => `chunks/${pathData.chunk.renderedHash}.js` }); } if (isServer) { output.library = { type: 'module' }; output.module = true; output.chunkFormat = 'module'; output.environment = { module: true }; output.iife = false; output.scriptType = 'module'; } const config = { mode: isProductionMode() ? 'production' : 'development', module: { rules: loaders }, target: isServer === true ? 'node' : 'web', output, plugins: [], cache: { type: 'memory' } }; if (isServer) { config.experiments = { outputModule: true }; } // Resolve aliases const alias = { '@evershop/evershop/components': path.resolve(__dirname, '../../components') }; if (theme) { alias['@components'] = [path.resolve(theme.path, 'dist/components')]; } else { alias['@components'] = []; } if ( !isRealDirectorySync( path.resolve(CONSTANTS.ROOTPATH, 'node_modules', '@evershop', 'evershop') ) ) { alias['@evershop/evershop'] = path.resolve( CONSTANTS.ROOTPATH, 'packages', 'evershop', 'dist' ); } // Resolve alias for extensions extenions.forEach((ext) => { alias['@components'].push(path.resolve(ext.resolve, 'dist/components')); }); alias['@components'].push(path.resolve(__dirname, '../../components')); // Avoid multiple react instances alias['react'] = path.resolve(CONSTANTS.ROOTPATH, 'node_modules/react'); alias['react-dom'] = path.resolve( CONSTANTS.ROOTPATH, 'node_modules/react-dom' ); alias['webpack-hot-middleware'] = path.resolve( CONSTANTS.ROOTPATH, 'node_modules/webpack-hot-middleware' ); config.resolve = { alias, extensions: ['.js', '.json', '.wasm'], extensionAlias: { '.jsx': ['.js'] }, fullySpecified: true }; config.optimization = {}; // Check if the flag --skip-minify is set const skipMinify = process.argv.includes('--skip-minify'); if (isProductionMode()) { config.optimization = Object.assign(config.optimization, { minimize: !skipMinify, minimizer: [ new SwcMinifyWebpackPlugin({ compress: true, mangle: true, module: true, sourceMap: true, keep_classnames: false, keep_fnames: false, safari10: true, sourceMap: true }) ] }); } return config; } ================================================ FILE: packages/evershop/src/lib/webpack/dev/createConfigClient.js ================================================ import path from 'path'; import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; import webpack from 'webpack'; import { getEnabledExtensions } from '../../../bin/extension/index.js'; import { CONSTANTS } from '../../helpers.js'; import { createBaseConfig } from '../createBaseConfig.js'; import { GraphqlPlugin } from '../plugins/GraphqlPlugin.js'; import { InjectTailwindSources } from '../plugins/InjectTailwindSources.js'; import { ThemeWatcherPlugin } from '../plugins/ThemeWatcherPlugin.js'; import { getTailwindSources } from '../util/getTailwindSources.js'; export function createConfigClient(isAdmin = false) { const extensions = getEnabledExtensions(); const tailwindSources = getTailwindSources(); const config = createBaseConfig(false); config.name = isAdmin ? 'bundle-client-admin' : 'bundle-client-frontstore'; // Set different output filenames for admin and frontstore to avoid conflicts config.output.filename = isAdmin ? 'admin-[name].js' : '[name].js'; config.output.publicPath = isAdmin ? '/backend/' : '/'; const loaders = config.module.rules; loaders.unshift({ test: /common[\\/]react[\\/]client[\\/]Index\.js$/i, use: [ { loader: path.resolve( CONSTANTS.LIBPATH, 'webpack/loaders/AreaLoader.js' ), options: { isAdmin } } ] }); loaders.push({ test: /\.css$/i, use: [ { loader: 'style-loader' }, { loader: 'css-loader', options: { url: false } }, { loader: 'postcss-loader', options: { postcssOptions: { plugins: [ InjectTailwindSources(tailwindSources), '@tailwindcss/postcss', 'autoprefixer' ] } } } ] }); loaders.push({ test: /\.scss$/i, use: [ { loader: 'style-loader' }, { loader: 'css-loader', options: { url: false } }, { loader: 'postcss-loader', options: { postcssOptions: { plugins: [ InjectTailwindSources(tailwindSources), '@tailwindcss/postcss', 'autoprefixer' ] } } }, { loader: 'sass-loader', options: { implementation: 'sass', api: 'modern' } } ] }); const { plugins } = config; plugins.push(new GraphqlPlugin(isAdmin)); plugins.push(new webpack.ProgressPlugin()); plugins.push(new webpack.HotModuleReplacementPlugin()); plugins.push( new ReactRefreshWebpackPlugin({ overlay: false }) ); plugins.push(new ThemeWatcherPlugin()); config.entry = () => { const entry = [ path.resolve( CONSTANTS.MODULESPATH, '../components/common/react/client/Index.js' ), isAdmin ? `webpack-hot-middleware/client?path=/__webpack_hmr_admin&reload=true&overlay=true` : `webpack-hot-middleware/client?path=/__webpack_hmr_frontstore&reload=true&overlay=true` ]; return entry; }; config.watchOptions = { aggregateTimeout: 300, ignored: new RegExp('(^|/)[a-z][^/]*.js$'), poll: 1000 }; // Enable source maps config.devtool = 'eval-cheap-module-source-map'; // Configure snapshot management for better caching // Exclude @evershop/evershop core and extensions in node_modules from managed paths // This ensures webpack watches for changes in these paths const nodeModuleExtensions = extensions .filter((ext) => ext.path && ext.path.includes('node_modules')) .map((ext) => { // Extract package name from path (e.g., @vendor/package or package-name) const match = ext.path.match( /node_modules[\\/](@[^/\\]+[\\/][^/\\]+|[^/\\]+)/ ); return match ? match[1].replace(/\\/g, '[\\\\/]') : null; }) .filter(Boolean) .join('|'); const managedPathsPattern = nodeModuleExtensions ? `^(.+?[\\\\/]node_modules[\\\\/](?!(@evershop[\\\\/]evershop|${nodeModuleExtensions}))(@.+?[\\\\/])?.+?)[\\\\/]` : `^(.+?[\\\\/]node_modules[\\\\/](?!(@evershop[\\\\/]evershop))(@.+?[\\\\/])?.+?)[\\\\/]`; config.snapshot = { managedPaths: [new RegExp(managedPathsPattern)] }; return config; } ================================================ FILE: packages/evershop/src/lib/webpack/getRouteBuildPath.js ================================================ import path from 'path'; import { CONSTANTS } from '../helpers.js'; import { getRouteBuildSubPath } from './getRouteBuildSubPath.js'; export function getRouteBuildPath(route) { const subPath = getRouteBuildSubPath(route); return path.resolve(CONSTANTS.BUILDPATH, subPath); } ================================================ FILE: packages/evershop/src/lib/webpack/getRouteBuildSubPath.js ================================================ export function getRouteBuildSubPath(route) { const { id, isAdmin } = route; return isAdmin === true ? `admin/${id}` : `frontStore/${id}`; } ================================================ FILE: packages/evershop/src/lib/webpack/isBuildRequired.ts ================================================ import { Route } from '../../types/route.js'; export const isBuildRequired = (route: Route) => { if (!route) { return false; } if (route.isApi || ['staticAsset', 'adminStaticAsset'].includes(route.id)) { return false; } else { return true; } }; ================================================ FILE: packages/evershop/src/lib/webpack/loaders/AreaLoader.js ================================================ import fs from 'fs'; import { pathToFileURL } from 'url'; import { inspect } from 'util'; import JSON5 from 'json5'; import { getEnabledWidgets } from '../../../lib/widget/widgetManager.js'; import { getAllRouteComponents } from '../../componee/getComponentsByRoute.js'; import { error } from '../../log/logger.js'; import { getRoutes } from '../../router/Router.js'; import { generateComponentKey } from '../../util/keyGenerator.js'; function buildComponentsPerRoute(components, imports) { const areas = {}; components.forEach((module) => { if (!fs.existsSync(module)) { return; } const source = fs.readFileSync(module, 'utf8'); // Regex matching 'export const layout = { ... }' const layoutRegex = /export\s+const\s+layout\s*=\s*{\s*areaId\s*:\s*['"]([^'"]+)['"],\s*sortOrder\s*:\s*(\d+)\s*,*\s*}/; const match = source.match(layoutRegex); if (match) { // Remove everything before '{' from the beginning of the match const check = match[0] .replace(/^[^{]*/, '') .replace(/(['"])?([a-zA-Z0-9_]+)(['"])?:/g, '"$2": '); try { const layout = JSON5.parse(check); const id = generateComponentKey(module); const url = pathToFileURL(module).toString(); // Check if this import already exists by url // Get all key of current imports const keys = Array.from(imports.keys()); const exists = keys.find((key) => key.url === url); if (!exists) { imports.set({ id, url }, `import ${id} from '${url}';`); } areas[layout.areaId] = areas[layout.areaId] || {}; areas[layout.areaId][id] = { id, sortOrder: layout.sortOrder, component: { default: `---${id}---` } }; } catch (e) { error(`Error parsing layout from ${module}`); error(e); } } }); return areas; } const buildWidgetComponentsPerRoute = (route, widgets, imports) => { const components = {}; widgets.forEach((widget) => { const componentPath = route.isAdmin ? widget.settingComponent : widget.component; const url = pathToFileURL(componentPath).toString(); // Check if this import already exists by url // Get all key of current imports const keys = Array.from(imports.keys()); const exists = keys.find((key) => key.url === url); const id = generateComponentKey( route.isAdmin ? `admin_widget_${widget.type}` : `widget_${widget.type}` ); if (!exists) { imports.set({ id: id, url }, `import ${id} from '${url}';`); } components[id] = { id: id, sortOrder: widget.sortOrder || 0, component: { default: `---${id}---` } }; }); return components; }; export default function AreaLoader(c) { const isAdmin = this.getOptions().isAdmin; this.cacheable(false); const components = getAllRouteComponents(isAdmin); const routes = getRoutes().filter( (route) => route.isApi === false && route.isAdmin === isAdmin ); const allRootComponents = {}; const widgets = getEnabledWidgets(); const imports = new Map(); // This map has a key as an object {id, url} to avoid duplicate imports try { Object.keys(components).forEach((routeId) => { allRootComponents[routeId] = buildComponentsPerRoute( components[routeId], imports ); const route = routes.find((r) => r.id === routeId); const widgetComponents = buildWidgetComponentsPerRoute( route, widgets, imports ); Object.assign(allRootComponents[routeId], { '*': widgetComponents }); }); } catch (e) { error('Error in AreaLoader:'); error(e); } const content = `${Array.from(imports.values()).join( '\r\n' )}\r\nconst components = ${inspect(allRootComponents, { depth: 5 }) .replace(/"---/g, '') .replace(/---"/g, '') .replace(/'---/g, '') .replace( /---'/g, '' )}\r\nArea.defaultProps.components = components[window.eContext.config.pageMeta.route.id] ;\r\n`; const result = c.replace('/** render */', content); return result; } ================================================ FILE: packages/evershop/src/lib/webpack/loaders/GraphQLAPILoader.js ================================================ export default function GraphqlAPILoader(source) { // Get options const options = this.getOptions(); const isAdmin = options.isAdmin || false; // Replace the specified code with an empty string if (isAdmin) { const newSource = source.replace( "url: '/api/graphql", "url: '/api/admin/graphql" ); return newSource; } else { return source; } } ================================================ FILE: packages/evershop/src/lib/webpack/loaders/GraphqlLoader.js ================================================ export default function GraphqlLoader(content) { // Regex matching 'export var query = `query { ... }`' const queryRegex = /export\s+var\s+query\s*=\s*`([^`]+)`/; const fragmentRegex = /export\s+var\s+fragment\s*=\s*`([^`]+)`/; const variablesRegex = /export\s+var\s+variables\s*=\s*`([^`]+)`/; return content .replace(queryRegex, '') .replace(fragmentRegex, '') .replace(variablesRegex, ''); } ================================================ FILE: packages/evershop/src/lib/webpack/loaders/LayoutLoader.js ================================================ export default function LayoutLoader(content) { // Regex matching 'export const layout = { ... }' const layoutRegex = /export\s+var\s+layout\s*=\s*{\s*areaId\s*:\s*['"]([^'"]+)['"],\s*sortOrder\s*:\s*(\d+)\s*,*\s*}/; return content.replace(layoutRegex, ''); } ================================================ FILE: packages/evershop/src/lib/webpack/loaders/StyleLoader.js ================================================ export default function StyleLoader() { return ''; } ================================================ FILE: packages/evershop/src/lib/webpack/loaders/TailwindLoader.js ================================================ import { join } from 'path'; import autoprefixer from 'autoprefixer'; import postcss from 'postcss'; import tailwindcss from 'tailwindcss'; import { getEnabledExtensions } from '../../../bin/extension/index.js'; import { getConfig } from '../../../lib/util/getConfig.js'; import { getEnabledTheme } from '../../../lib/util/getEnabledTheme.js'; import { CONSTANTS } from '../../helpers.js'; import { getTailwindConfig } from '../util/getTailwindConfig.js'; export default async function TailwindLoader(c) { this.cacheable(false); if (this.mode === 'production') { if (this.resourcePath.includes('tailwind.scss')) { return `/*beginTailwind*/${c}/*endTailwind*/`; } else { return c; } } const components = this.getOptions().getComponents(); const { route } = this.getOptions(); components.forEach((module) => { this.addDependency(module); }); if (!this.resourcePath.includes('tailwind.scss')) { return c; } const mergedTailwindConfig = await getTailwindConfig(route); const enabledExtensions = getEnabledExtensions(); mergedTailwindConfig.content = [ // All file in packages/evershop/dist and name is capitalized join(CONSTANTS.ROOTPATH, 'packages', 'evershop', 'dist', '**', '[A-Z]*.js'), // All file in node_modules/@evershop/evershop/dist and name is capitalized join( CONSTANTS.ROOTPATH, 'node_modules', '@evershop', 'evershop', 'dist', '**', '[A-Z]*.js' ), ...enabledExtensions.map((extension) => join(extension.path, 'dist', '**', '[A-Z]*.js') ) ]; const theme = getEnabledTheme(); if (theme) { mergedTailwindConfig.content.push( join(theme.path, 'dist', '**', '[A-Z]*.js') ); } return postcss([tailwindcss(mergedTailwindConfig), autoprefixer]) .process(c, { from: undefined }) .then((result) => result.css); } ================================================ FILE: packages/evershop/src/lib/webpack/loaders/TranslationLoader.js ================================================ export default async function TranslationLoader(c) { const csvData = await this.getOptions().getTranslateData(); // Use regex to find all function call `_()` in the template string const regex = /_\s*\(\s*(?"[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*'|\w+)\s*(?:,\s*(?null|\{[\s\S]*?\}|"[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*'|\w+)\s*)?\)/g; let result = c; // Loop through each function call and get the template string let match; while ((match = regex.exec(c)) !== null) { let template = match.groups.arg1; // Remove the quote from the start and end of the template string template = template.replace(/^["']/, '').replace(/["']$/, ''); const newValue = csvData[template]; // Check if the template is exist in the csvData if (newValue) { result = result.replace(match[0], `_("${newValue}",${match[2] || null})`); } } return result; } ================================================ FILE: packages/evershop/src/lib/webpack/loaders/loadTranslationFromCsv.ts ================================================ import fs from 'fs'; import path from 'path'; import { CONSTANTS } from '../../helpers.js'; import { error } from '../../log/logger.js'; import { getConfig } from '../../util/getConfig.js'; import { readCsvFile } from '../../util/readCsvFile.js'; export async function loadCsvTranslationFiles(): Promise< Record > { try { const language = getConfig('shop.language', 'en'); const folderPath = path.resolve( CONSTANTS.ROOTPATH, 'translations', language ); // Check if path exists if (!fs.existsSync(folderPath)) { return {}; } const results = {}; const files = await fs.promises.readdir(folderPath); const csvFiles = files.filter((file) => path.extname(file) === '.csv'); const filePromises = csvFiles.map((file) => { const filePath = path.join(folderPath, file); return readCsvFile(filePath); }); const fileDataList = await Promise.all(filePromises); for (const fileData of fileDataList) { for (const [key, value] of Object.entries(fileData)) { results[key] = value; } } return results; } catch (err) { error(err); return {}; } } ================================================ FILE: packages/evershop/src/lib/webpack/plugins/FileListPlugin.js ================================================ import fs from 'fs'; export const FileListPlugin = class FileListPlugin { apply(compiler) { compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => { const list = compilation._modules; const modules = []; list.forEach((element) => { modules.push(element.resource); }); // Create a header string for the generated file: let filelist = 'CSS:\n\n'; // Loop through all compiled assets, // adding a new line item for each filename. modules.forEach((m) => { if (m) { const path = m.replace('.js', '.css'); if (fs.existsSync(path)) filelist += `${fs.readFileSync(path, 'utf-8')}\n`; } }); // Insert this list into the webpack build as a new file asset: compilation.assets['filelist.md'] = { source() { return filelist; }, size() { return filelist.length; } }; callback(); }); } }; ================================================ FILE: packages/evershop/src/lib/webpack/plugins/GraphqlPlugin.js ================================================ import { getAllRouteComponents, getComponentsByRoute } from '../../componee/getComponentsByRoute.js'; import { parseGraphql } from '../util/parseGraphql.js'; export const GraphqlPlugin = class GraphqlPlugin { constructor(isAdmin = false) { this.isAdmin = isAdmin; this.query = {}; this.fragments = {}; this.variables = []; } apply(compiler) { const { webpack } = compiler; const { RawSource } = webpack.sources; compiler.hooks.thisCompilation.tap('GraphqlPlugin', (compilation) => { // TODO: Can we get list of module without calling getComponentsByRoute again? const components = getAllRouteComponents(this.isAdmin); // Store one file per route instead of a single file Object.keys(components).forEach((routeId) => { const routeGraphqlQueries = parseGraphql(components[routeId]); const filename = `query-${routeId}.graphql`; compilation.emitAsset( filename, new RawSource(JSON.stringify(routeGraphqlQueries, null, 2)) ); }); }); } }; ================================================ FILE: packages/evershop/src/lib/webpack/plugins/InjectTailwindSources.ts ================================================ // Inject Tailwind @source directives for Tailwind v4 const InjectTailwindSources = (sources) => { const uniqueSources = Array.from(new Set(sources)); const plugin = () => ({ postcssPlugin: 'inject-tailwind-sources', Once(root) { // Only inject if this CSS imports Tailwind const hasTailwindImport = root.nodes.some( (node) => node.type === 'atrule' && node.name === 'import' && node.params.includes('tailwindcss') ); if (!hasTailwindImport) return; // Prepend @source entries so Tailwind scans the intended files uniqueSources.forEach((src) => { root.prepend(`@source "${src}";`); }); } }); plugin.postcss = true; return plugin; }; export { InjectTailwindSources }; ================================================ FILE: packages/evershop/src/lib/webpack/plugins/Tailwindcss.ts ================================================ import fs from 'fs'; import path from 'path'; import tailwindcss from '@tailwindcss/postcss'; import CleanCSS from 'clean-css'; import postcss from 'postcss'; import webpack from 'webpack'; import { Route } from '../../../types/route.js'; import { error } from '../../log/logger.js'; type WebpackCompiler = webpack.Compiler; type WebpackCompilation = webpack.Compilation; type WebpackAssets = webpack.Compilation['assets']; export class Tailwindcss { private route: Route; constructor(route: Route) { this.route = route; } apply(compiler: WebpackCompiler): void { compiler.hooks.afterEmit.tapAsync( 'Tailwindcss', async (compilation: WebpackCompilation, callback) => { try { await this.processRouteAssetsAfterEmit(compilation); callback(); } catch (err) { callback(err as Error); } } ); } async processRouteAssetsAfterEmit( compilation: WebpackCompilation ): Promise { const outputPath = compilation.outputOptions.path; if (!outputPath) { return; } const processingPromises: Promise[] = []; const routeCSSAssets = this.getRouteCSSAssets( compilation.assets, this.route ); for (const cssAsset of routeCSSAssets) { processingPromises.push( this.processCSSWithTailwindFromDisk(cssAsset, outputPath) ); } await Promise.all(processingPromises); } getRouteCSSAssets(assets: WebpackAssets, route: Route): string[] { const results = Object.keys(assets).filter((name) => { // Normalize path separators for cross-platform compatibility const normalizedName = name.replace(/\\/g, '/'); return ( (normalizedName.includes(route.id + '/') || /^\d/.test(normalizedName)) && normalizedName.endsWith('.css') ); }); return results; } async processCSSWithTailwindFromDisk( cssAssetName: string, outputPath: string ): Promise { const cssFilePath = path.resolve(outputPath, cssAssetName); // Read the CSS file from disk if (!fs.existsSync(cssFilePath)) { error(`CSS file not found: ${cssFilePath}`); return; } const originalCSS = fs.readFileSync(cssFilePath, 'utf-8'); // Process CSS with Tailwind let processedCSS = originalCSS; if (cssAssetName.includes(this.route.id)) { // Get the directory where the CSS file is located const cssDir = path.dirname(cssFilePath); // Find all JS files in the same directory (already on disk) const jsFiles = fs .readdirSync(cssDir) .filter((file) => file.endsWith('.js')); if (jsFiles.length === 0) { error(`No JS files found in ${cssDir}`); return; } // Reference the JS files for Tailwind scanning const sourceDirectives = jsFiles .map((file) => `@source "./${file}";`) .join('\n'); processedCSS = `${sourceDirectives} ${originalCSS}`; } try { // Process with Tailwind const result = await postcss([tailwindcss()]).process(processedCSS, { from: cssFilePath }); // Minify the result const cleanCSS = new CleanCSS({ level: 2, returnPromise: true }); const minified = await cleanCSS.minify(result.css); // Write the processed CSS back to disk fs.writeFileSync(cssFilePath, minified.styles, 'utf-8'); } catch (e) { error(e); } } } ================================================ FILE: packages/evershop/src/lib/webpack/plugins/ThemeWatcherPlugin.ts ================================================ import path from 'path'; import watcher from '@parcel/watcher'; import touch from 'touch'; import type { Compiler, WebpackPluginInstance } from 'webpack'; import { getEnabledExtensions } from '../../../bin/extension/index.js'; import { CONSTANTS } from '../../helpers.js'; import { debug } from '../../log/logger.js'; import { getEnabledTheme } from '../../util/getEnabledTheme.js'; interface AsyncWebpackSubscription { unsubscribe(): Promise; } declare module 'webpack' { interface Module { resource?: string; } } let globalWatcher: AsyncWebpackSubscription | null = null; const watcherSubscribers = new Set(); export class ThemeWatcherPlugin implements WebpackPluginInstance { private pendingFiles: Set; constructor() { this.pendingFiles = new Set(); } apply(compiler: Compiler): void { if (compiler.options.mode !== 'development') { return; } const theme = getEnabledTheme(); if (!theme) { return; } watcherSubscribers.add(compiler); if (!globalWatcher) { this.initializeGlobalWatcher(); } compiler.hooks.compilation.tap('ThemeWatcherPlugin', (compilation) => { compilation.hooks.finishModules.tap( 'ThemeWatcherPlugin', (modules: Set) => { if (this.pendingFiles.size === 0) { return; } const extensions = getEnabledExtensions(); const watchPath = path.join(theme.path, 'dist', 'components'); const filesToProcess = Array.from(this.pendingFiles); this.pendingFiles.clear(); // Clear immediately to prevent loops filesToProcess.forEach((filePath: string) => { const relativePath = path.relative(watchPath, filePath); let targetModule: any = null; let targetPath: string | null = null; for (const extension of extensions) { const extensionComponentPath = path.resolve( extension.resolve, 'dist/components', relativePath ); targetModule = Array.from(modules).find( (module) => module.resource && module.resource === extensionComponentPath ); if (targetModule) { targetPath = extensionComponentPath; break; } } if (!targetModule) { const coreComponentPath = path.resolve( CONSTANTS.MODULESPATH, '../components', relativePath ); targetModule = Array.from(modules).find( (module) => module.resource && module.resource === coreComponentPath ); if (targetModule) { targetPath = coreComponentPath; } } if (targetModule) { const issuers: any[] = []; for (const module of modules) { if (module.dependencies) { for (const dependency of module.dependencies) { const depModule = compilation.moduleGraph.getModule(dependency); if (depModule === targetModule) { issuers.push(module); break; } } } } if (issuers.length > 0) { for (const issuer of issuers) { if (issuer.resource) { touch.sync(issuer.resource); } } } } }); } ); }); compiler.hooks.watchClose.tap('ThemeWatcherPlugin', () => { watcherSubscribers.delete(compiler); if (watcherSubscribers.size === 0) { this.cleanupGlobalWatcher(); } }); } private initializeGlobalWatcher(): void { const theme = getEnabledTheme(); if (!theme) return; const watchPath = path.join(theme.path, 'dist', 'components'); watcher .subscribe(watchPath, (err: Error | null, events: any[]) => { if (err) { debug(err); return; } const createEvents = events.filter((event) => event.type === 'create'); if (createEvents.length > 0) { watcherSubscribers.forEach((compiler: Compiler) => { const plugin = compiler.options.plugins?.find( (p: any) => p instanceof ThemeWatcherPlugin ) as ThemeWatcherPlugin | undefined; if (plugin) { createEvents.forEach((event) => { plugin.pendingFiles.add(event.path); }); if (compiler.watching) { compiler.watching.invalidate(); } } }); } }) .then((subscription: AsyncWebpackSubscription) => { globalWatcher = subscription; }) .catch((error: Error) => { debug(error); }); } private cleanupGlobalWatcher(): void { if (globalWatcher) { globalWatcher.unsubscribe(); globalWatcher = null; } } } ================================================ FILE: packages/evershop/src/lib/webpack/prod/createConfigClient.js ================================================ import path from 'path'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import WebpackBar from 'webpackbar'; import { getEnabledExtensions } from '../../../bin/extension/index.js'; import { CONSTANTS } from '../../helpers.js'; import { createBaseConfig } from '../createBaseConfig.js'; import { getRouteBuildPath } from '../getRouteBuildPath.js'; import { getRouteBuildSubPath } from '../getRouteBuildSubPath.js'; import { isBuildRequired } from '../isBuildRequired.js'; import { InjectTailwindSources } from '../plugins/InjectTailwindSources.js'; import { getTailwindSources } from '../util/getTailwindSources.js'; export function createConfigClient(routes) { const extenions = getEnabledExtensions(); const config = createBaseConfig(false); const tailwindSources = getTailwindSources(); const { plugins } = config; const entry = {}; routes.forEach((route) => { if (!isBuildRequired(route)) { return; } const subPath = getRouteBuildSubPath(route); entry[subPath] = [ path.resolve(CONSTANTS.BUILDPATH, subPath, 'client', 'entry.js') ]; plugins.push( new HtmlWebpackPlugin({ templateContent: ({ htmlWebpackPlugin }) => { const jsFiles = htmlWebpackPlugin.files.js; const cssFiles = htmlWebpackPlugin.files.css; // Filter out the incorrect vendor chunk based on route type const filteredJsFiles = jsFiles.filter((file) => { if (route.isAdmin) { // For admin routes, exclude frontstore-vendor return !file.includes('/frontstore-vendor/'); } else { // For frontStore routes, exclude admin-vendor return !file.includes('/admin-vendor/'); } }); return JSON.stringify({ js: filteredJsFiles, css: cssFiles }); }, filename: path.resolve( getRouteBuildPath(route), 'client', 'index.json' ), chunks: (() => { // Only 2 vendor chunks now - no shared vendor const chunks = ['common', subPath]; // Add context-specific vendor and components if (route.isAdmin) { chunks.unshift('admin-vendor'); // Add admin vendor first chunks.splice(-1, 0, 'admin-components'); // Insert admin components before route-specific chunk } else { chunks.unshift('frontstore-vendor'); // Add frontstore vendor first chunks.splice(-1, 0, 'frontstore-components'); // Insert frontstore components before route-specific chunk } return chunks; })(), chunksSortMode: 'manual', inject: false, publicPath: '/assets/' }) ); //plugins.push(new Tailwindcss(route)); }); const loaders = config.module.rules; loaders.push({ test: /\.css$/i, use: [ MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { url: false } }, { loader: 'postcss-loader', options: { postcssOptions: { plugins: [ InjectTailwindSources(tailwindSources), '@tailwindcss/postcss', 'autoprefixer' ] } } } ] }); loaders.push({ test: /\.scss$/i, use: [ MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { url: false } }, { loader: 'postcss-loader', options: { postcssOptions: { plugins: [ InjectTailwindSources(tailwindSources), '@tailwindcss/postcss', 'autoprefixer' ] } } }, { loader: 'sass-loader', options: { implementation: 'sass', api: 'modern' } } ] }); plugins.push(new WebpackBar({ name: 'Client' })); plugins.push( new MiniCssExtractPlugin({ filename: '[name]/client/[contenthash].css', chunkFilename: '[name]/client/[id].[contenthash].css' }) ); config.entry = entry; config.output.filename = '[name]/client/[fullhash].js'; config.name = 'Client'; config.optimization = { ...config.optimization, splitChunks: { chunks: 'all', cacheGroups: { // Admin vendor chunk - includes third-party node_modules used by admin routes // Excludes @evershop/evershop core and extensions adminVendor: { test: (module) => { // Only match JS modules from node_modules, exclude CSS if (module.type === 'css/mini-extract') { return false; } // Must be from node_modules if (!module.resource) { return false; } // Normalize path separators for cross-platform compatibility const normalizedResource = module.resource.replace(/\\/g, '/'); if (!normalizedResource.includes('node_modules')) { return false; } // Exclude @evershop/evershop core package if ( normalizedResource.includes('node_modules/@evershop/evershop') ) { return false; } // Exclude extensions (they may be npm packages in node_modules) for (const ext of extenions) { if (ext.resolve) { const normalizedExtResolve = ext.resolve.replace(/\\/g, '/'); if (normalizedResource.includes(normalizedExtResolve)) { return false; } } } return true; }, chunks: (chunk) => { // Only split from admin route chunks const route = routes.find((r) => { const subPath = getRouteBuildSubPath(r); return chunk.name === subPath; }); return route && route.isAdmin; }, name: 'admin-vendor', enforce: true, priority: 20 }, // FrontStore vendor chunk - includes third-party node_modules used by frontStore routes // Excludes @evershop/evershop core and extensions frontStoreVendor: { test: (module) => { // Only match JS modules from node_modules, exclude CSS if (module.type === 'css/mini-extract') { return false; } // Must be from node_modules if (!module.resource) { return false; } // Normalize path separators for cross-platform compatibility const normalizedResource = module.resource.replace(/\\/g, '/'); if (!normalizedResource.includes('node_modules')) { return false; } // Exclude @evershop/evershop core package if ( normalizedResource.includes('node_modules/@evershop/evershop') ) { return false; } // Exclude extensions (they may be npm packages in node_modules) for (const ext of extenions) { if (ext.resolve) { const normalizedExtResolve = ext.resolve.replace(/\\/g, '/'); if (normalizedResource.includes(normalizedExtResolve)) { return false; } } } return true; }, chunks: (chunk) => { // Only split from frontStore route chunks const route = routes.find((r) => { const subPath = getRouteBuildSubPath(r); return chunk.name === subPath; }); return route && !route.isAdmin; }, name: 'frontstore-vendor', enforce: true, priority: 20 }, // Common chunk for @components/common (shared across all routes) common: { test: /[\\/]@components[\\/]common[\\/]/, name: 'common', chunks: 'all', enforce: true, priority: 15 }, // Admin components chunk (only for admin routes) adminComponents: { test: /[\\/]@components[\\/]admin[\\/]/, name: 'admin-components', chunks: 'all', enforce: true, priority: 10 }, // FrontStore components chunk (only for frontStore routes) frontStoreComponents: { test: /[\\/]@components[\\/]frontStore[\\/]/, name: 'frontstore-components', chunks: 'all', enforce: true, priority: 10 }, // Default group to prevent CSS duplication across chunks default: false, defaultVendors: false } } }; return config; } ================================================ FILE: packages/evershop/src/lib/webpack/prod/createConfigServer.js ================================================ import path from 'path'; import WebpackBar from 'webpackbar'; import { CONSTANTS } from '../../helpers.js'; import { createBaseConfig } from '../createBaseConfig.js'; import { getRouteBuildSubPath } from '../getRouteBuildSubPath.js'; import { isBuildRequired } from '../isBuildRequired.js'; export function createConfigServer(routes) { const entry = {}; routes.forEach((route) => { if (!isBuildRequired(route)) { return; } const subPath = getRouteBuildSubPath(route); entry[subPath] = [ path.resolve(CONSTANTS.BUILDPATH, subPath, 'server', 'entry.js') ]; }); const config = createBaseConfig(true); const { plugins } = config; plugins.push(new WebpackBar({ name: 'Server', color: 'orange' })); const loaders = config.module.rules; loaders.push({ test: /\.(css|scss)$/i, use: [ { loader: path.resolve( CONSTANTS.LIBPATH, 'webpack/loaders/StyleLoader.js' ) } ] }); config.entry = entry; config.name = 'Server'; return config; } ================================================ FILE: packages/evershop/src/lib/webpack/resolveAlias.js ================================================ import fs from 'fs'; import path from 'path'; function getAllFilesInFolder(folderPath) { if (!fs.existsSync(folderPath)) { return {}; } let results = {}; // Get the contents of the folder const contents = fs.readdirSync(folderPath); // Loop through each item in the folder for (const item of contents) { const itemPath = path.join(folderPath, item); // Check if the item is a directory if (fs.lstatSync(itemPath).isDirectory()) { // Recursively call the function to get the files in the subdirectory const subResults = getAllFilesInFolder(itemPath); results = { ...results, ...subResults }; } else if ( (/.js$/.test(item) && /^[A-Z]/.test(item[0])) || /\.(css|scss)$/i.test(item) ) { const pathParts = itemPath.split(path.sep); // Find the index of the "components" directory const componentsIndex = pathParts.indexOf('components'); // Return the part of the path after the "components" directory const alias = path .join('@components', ...pathParts.slice(componentsIndex + 1)) .replace('.js', '') .replace('.scss', '') .replace('.css', ''); results[alias] = itemPath; } } return results; } export function resolveAlias(extensions = [], themePath = null) { let resolves = {}; if (themePath) { resolves = getAllFilesInFolder(path.resolve(themePath, 'components')); } // loop through the extensions and get the files extensions.forEach((extension) => { const extensionFiles = getAllFilesInFolder( path.resolve(extension.path, 'components') ); resolves = { ...extensionFiles, ...resolves }; }); return resolves; } export const alias = {}; ================================================ FILE: packages/evershop/src/lib/webpack/tests/unit/resolveAlias.test.js ================================================ import path from 'path'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; import { resolveAlias } from '../../resolveAlias.js'; // Get the directory name for this file const currentFilePath = fileURLToPath(import.meta.url); const currentDirPath = dirname(currentFilePath); describe('resolveAlias', () => { it('It should get the components and css file with correct priority', () => { const resolves = resolveAlias( [ { path: path.resolve(currentDirPath, 'extensions/extensionA'), priority: 1 }, { path: path.resolve(currentDirPath, 'extensions/extensionB'), priority: 2 } ], path.resolve(currentDirPath, 'theme') ); expect(resolves[path.join('@components', 'a', 'A')]) .toString() .includes('theme'); expect(resolves[path.join('@components', 'a', 'a')]) .toString() .includes('theme'); expect(resolves[path.join('@components', 'b', 'bb')]) .toString() .includes('theme'); expect(resolves[path.join('@components', 'b', 'bb', 'BB')]) .toString() .includes('theme'); expect(resolves[path.join('@components', 'd', 'D')]) .toString() .includes('extensionA'); expect(resolves[path.join('@components', 'd', 'dd', 'DD')]) .toString() .includes('extensionA'); expect(resolves[path.join('@components', 'e', 'E')]) .toString() .includes('extensionB'); expect(resolves[path.join('@components', 'e', 'ee', 'EE')]) .toString() .includes('extensionB'); }); }); ================================================ FILE: packages/evershop/src/lib/webpack/tests/unit/theme/components/a/A.jsx ================================================ ================================================ FILE: packages/evershop/src/lib/webpack/tests/unit/theme/components/a/a.scss ================================================ ================================================ FILE: packages/evershop/src/lib/webpack/tests/unit/theme/components/b/B.jsx ================================================ ================================================ FILE: packages/evershop/src/lib/webpack/tests/unit/theme/components/b/B.scss ================================================ ================================================ FILE: packages/evershop/src/lib/webpack/tests/unit/theme/components/b/bb/BB.jsx ================================================ ================================================ FILE: packages/evershop/src/lib/webpack/util/getTailwindConfig.js ================================================ import fs from 'fs'; import { join } from 'path'; import { pathToFileURL } from 'url'; import { getEnabledTheme } from '../../../lib/util/getEnabledTheme.js'; /** * @deprecated This function is deprecated and will be removed in future versions. */ export async function getTailwindConfig(isAdmin = false) { const defaultTailwindConfig = isAdmin ? await import('../../../modules/cms/services/tailwind.admin.config.js') : await import( '../../../modules/cms/services/tailwind.frontStore.config.js' ); let tailwindConfig = {}; if (!isAdmin) { // Get the current theme const theme = getEnabledTheme(); if ( theme && fs.existsSync(join(theme.path, 'dist', 'tailwind.config.js')) ) { tailwindConfig = await import( pathToFileURL(join(theme.path, 'dist', 'tailwind.config.js')) ); } } // Merge defaultTailwindConfig with tailwindConfigJs const mergedTailwindConfig = Object.assign( defaultTailwindConfig.default, tailwindConfig.default ); return mergedTailwindConfig; } ================================================ FILE: packages/evershop/src/lib/webpack/util/getTailwindSources.ts ================================================ import path from 'path'; import { getEnabledExtensions } from '../../../bin/extension/index.js'; import { CONSTANTS } from '../../helpers.js'; import { getEnabledTheme } from '../../util/getEnabledTheme.js'; export function getTailwindSources(): string[] { const sources: string[] = []; // Add the core source sources.push( path.resolve(CONSTANTS.MODULESPATH, '..', '**/*.{js,jsx,ts,tsx}') ); // Add enabled extensions const extensions = getEnabledExtensions(); for (const extension of extensions) { sources.push(path.resolve(extension.path, '**/*.{js,jsx,ts,tsx}')); } // Add enabled theme const theme = getEnabledTheme(); if (theme) { sources.push(path.resolve(theme.path, '**/*.{js,jsx,ts,tsx}')); } return sources.map((s) => s.replace(/\\/g, '/')); } ================================================ FILE: packages/evershop/src/lib/webpack/util/parseGraphql.js ================================================ import fs from 'fs'; import JSON5 from 'json5'; import uniqid from 'uniqid'; import { isResolvable } from '../../util/isResolvable.js'; import { generateComponentKey } from '../../util/keyGenerator.js'; import { parseGraphqlByFile } from './parseGraphqlByFile.js'; export function parseGraphql(modules) { let inUsedFragments = []; const propsMap = {}; let queries = {}; let fragmentStr = ''; const variableList = {}; modules.forEach((module) => { if (!fs.existsSync(module) && !isResolvable(module)) { return; } const variables = { values: {}, defs: [] }; let modulePath; let moduleKey; // If the module is resolvable, get the apsolute path if (!fs.existsSync(module)) { modulePath = new URL(import.meta.resolve(module)).pathname; moduleKey = generateComponentKey(module); } else { modulePath = module; moduleKey = generateComponentKey(modulePath); } const moduleGraphqlData = parseGraphqlByFile(modulePath); queries[moduleKey] = moduleGraphqlData.query.source; fragmentStr += `\n${moduleGraphqlData.fragments.source}`; Object.assign( variables.values, JSON5.parse(moduleGraphqlData.variables.source) ); variables.defs = variables.defs.concat( moduleGraphqlData.variables.definitions ); variableList[moduleKey] = variables; propsMap[moduleKey] = moduleGraphqlData.query.props; inUsedFragments = inUsedFragments.concat(moduleGraphqlData.fragments.pairs); }); // Process fragments const extraFragments = []; inUsedFragments.forEach((fragment) => { // Check if there was a fragment with same name and type already processed const f = extraFragments.find( (ar) => ar.name === fragment.name && ar.type === fragment.type ); if (f) { // Replace fragment alias with the one already processed const regex = new RegExp(`\\.\\.\\.([ ]+)?${fragment.alias}`, 'g'); queries = Object.keys(queries).reduce((acc, key) => { acc[key] = queries[key].replace(regex, `...${f.alias}`); return acc; }, {}); fragmentStr = fragmentStr.replace(regex, `...${f.alias}`); } else { const regex = new RegExp( `fragment([ ]+)${fragment.name}([ ]+)on([ ]+)${fragment.type}`, 'g' ); fragmentStr = fragmentStr.replace(regex, () => { const alias = `${fragment.name}_${uniqid()}`; // Check if there is a fragment with the same name and type const frm = extraFragments.find( (ar) => ar.name === fragment.name && ar.type === fragment.type ); if (frm) { frm.child.push(alias); } else { extraFragments.push({ name: fragment.name, alias: fragment.alias, type: fragment.type, child: [alias] }); } return `fragment ${alias} on ${fragment.type}`; }); } }); extraFragments.forEach((fragment) => { fragmentStr += `\nfragment ${fragment.alias} on ${ fragment.type } {\n ${fragment.child.map((c) => `...${c}`).join('\n')} \n}`; }); return { queries, fragments: fragmentStr, variables: variableList, propsMap }; } ================================================ FILE: packages/evershop/src/lib/webpack/util/parseGraphqlByFile.js ================================================ import fs from 'fs'; import { parse } from 'graphql'; import { print } from 'graphql/language/printer.js'; import JSON5 from 'json5'; import uniqid from 'uniqid'; // This function should return an object { query, fragments, variables }. export function parseGraphqlByFile(module) { const result = { query: {}, fragments: {}, variables: {} }; const variables = []; const fileSource = fs.readFileSync(module, 'utf8'); /** Process query */ // Regex matching export const query = `...` or export const query = "" or export const query = '' const queryRegex = /export\s+const\s+query\s*=\s*`([^`]+)`|export\s+const\s+query\s*=\s*["']([^"']+)["']/g; const queryMatch = fileSource.match(queryRegex); if (queryMatch) { let queryBody = queryMatch[0].replace( queryRegex, (match, p1, p2) => p1 || p2 ); queryBody = queryBody.replace( /getContextValue\(([^)]+)\)/g, (match, p1) => { const base64 = Buffer.from(p1).toString('base64'); return `"getContextValue_${base64}"`; } ); queryBody = queryBody.replace( /getWidgetSetting\(([^)]+)\)/g, (match, p1) => { const base64 = Buffer.from(p1).toString('base64'); return `"getWidgetSetting_${base64}"`; } ); const queryAst = parse(queryBody); const map = queryAst.definitions[0].selectionSet.selections.map( (selection) => { const name = selection.name.value; const alias = selection.alias ? selection.alias.value : name; const newAlias = `e${uniqid()}`; if (!selection.alias) { selection.alias = { kind: 'Name', value: newAlias }; } else { selection.alias.value = newAlias; } return { origin: alias, alias: newAlias }; } ); // Get back the new query string queryBody = print(queryAst); // Regex to find all variable name and type ($name: Type!) in graphql query const variableRegex = /\$([a-zA-Z0-9]+)\s*:\s*([a-zA-Z0-9\[\]!]+)/g; const variableMatch = queryBody.match(variableRegex); if (variableMatch) { variableMatch.forEach((variable) => { const varRegex = /\$([a-zA-Z0-9]+)\s*:\s*([a-zA-Z0-9\[\]!]+)/; const varMatch = varRegex.exec(variable); const name = varMatch[1]; const type = varMatch[2]; variables.push({ origin: name, type, alias: `variable_${uniqid()}` }); }); } // Replace all variable in graphql query variables.forEach((variable) => { // Use word boundary to ensure we match the complete variable name only // This prevents partial matches like 'count' matching inside 'countPerRow' const regex = new RegExp(`\\$${variable.origin}\\b`, 'g'); queryBody = queryBody.replace(regex, `$${variable.alias}`); }); // Use slice function to get everything between the first '{' and the last '}' in the query queryBody = queryBody.slice( queryBody.indexOf('{') + 1, queryBody.lastIndexOf('}') ); result.query.source = queryBody; result.query.props = map; } else { result.query.source = ''; result.query.props = []; } /** Process fragments */ // Regex matching export const query = `...` or export const query = "" or export const query = '' const fragmentsRegex = /export\s+const\s+fragments\s*=\s*`([^`]+)`|export\s+const\s+fragments\s*=\s*["']([^"']+)["']/g; const fragmentsMatch = fileSource.match(fragmentsRegex); const fragmentNames = []; if (fragmentsMatch) { const fragmentsBody = fragmentsMatch[0].replace( fragmentsRegex, (match, p1, p2) => p1 || p2 ); const fragmentsAst = parse(fragmentsBody); fragmentsAst.definitions.forEach((fragment) => { if (fragment.kind === 'FragmentDefinition') { fragmentNames.push({ name: fragment.name.value, type: fragment.typeCondition.name.value }); } else { throw new Error( `Only fragments are allowed in 'export const fragments = \`...\`. Error in ${module}` ); } }); result.fragments.source = fragmentsBody; } else { result.fragments.source = ''; } // Using regex to get all fragment consumption (e.g. ...fragmentName) const fragmentConsumptions = ( result.query.source.match(/\.\.\.([ ]+)?([a-zA-Z0-9_]+)/g) || [] ).concat( result.fragments.source.match(/\.\.\.([ ]+)?([a-zA-Z0-9_]+)/g) || [] ); // Deduplicate fragment consumptions to handle multiple usages of the same fragment const uniqueFragmentNames = [ ...new Set( fragmentConsumptions.map((consumption) => consumption.replace(/\.\.\.([ ]+)?/, '') ) ) ]; if (uniqueFragmentNames.length > 0) { uniqueFragmentNames.forEach((fragmentName) => { const fragment = fragmentNames.find((f) => f.name === fragmentName); if (!fragment) { throw new Error( `Fragment '${fragmentName}' is not defined in ${module}` ); } else { result.fragments.pairs = result.fragments.pairs || []; const alias = `${fragmentName}_${uniqid()}`; const regex = new RegExp(`\\.\\.\\.([ ]+)?${fragmentName}\\b`, 'g'); // Replace in query source with alias result.query.source = result.query.source.replace(regex, `...${alias}`); // Replace in fragment source with alias result.fragments.source = result.fragments.source.replace( regex, `...${alias}` ); result.fragments.pairs.push({ name: fragmentName, alias, type: fragment.type }); } }); } else { result.fragments.pairs = []; } /** Processing variables */ // Regex matching export const variables = `{ ... }` const variablesRegex = /export\s+const\s+variables\s*=\s*`([^`]+)`/g; const variablesMatch = fileSource.match(variablesRegex); if (variablesMatch) { let variablesBody = variablesMatch[0].replace( variablesRegex, (match, p1) => p1 ); variablesBody = variablesBody.replace( /getContextValue\(([^)]+)\)/g, (match, p1) => { const base64 = Buffer.from(p1).toString('base64'); return `"getContextValue_${base64}"`; } ); variablesBody = variablesBody.replace( /getWidgetSetting\(([^)]*)\)/g, (match, p1) => { const base64 = Buffer.from(p1).toString('base64'); return `"getWidgetSetting_${base64}"`; } ); try { // Json parse the variables body const variablesJson = JSON5.parse(variablesBody); // Replace all variable in graphql query Object.keys(variablesJson).forEach((variableName) => { const variable = variables.find((v) => v.origin === variableName); if (variable) { variablesJson[variable.alias] = variablesJson[variableName]; delete variablesJson[variableName]; } }); result.variables.source = JSON5.stringify(variablesJson); result.variables.definitions = variables; } catch (e) { throw new Error(`Invalid variables in ${module}`); } } else { result.variables.source = '{}'; result.variables.definitions = []; } return result; } ================================================ FILE: packages/evershop/src/lib/widget/tests/unit/widgetManager.test.js ================================================ import { jest, describe, it, expect, beforeAll, afterAll } from '@jest/globals'; const getValidWidget = (type = 'TestWidgetType') => ({ name: `Widget ${type}`, type: type, description: `Description for ${type}`, settingComponent: `${type}Settings.js`, component: `${type}Component.js`, enabled: true, defaultSettings: { theme: 'light' } }); jest.unstable_mockModule('fs', () => ({ existsSync: jest.fn((path) => { if ( path.includes('InvalidComponent.js') || path.includes('InvalidSetting.js') ) { return false; // Simulate unresolvable paths } return true; // Simulate valid paths for other components }), statSync: jest.fn(() => ({ isFile: () => true })) })); const realPath = await import('path'); jest.unstable_mockModule('path', () => ({ default: true, ...realPath, resolve: jest.fn((...args) => `/mocked/path/${args.join('/')}`) })); describe('Widget Manager Module', () => { beforeEach(async () => { jest.resetModules(); // Reset modules before each test to ensure fresh mocks }); afterEach(() => { jest.clearAllMocks(); // Clear mock call history after each test }); // --- Test _isFrozen state enforcement --- describe('Mutation after getAllWidgets()', () => { it('should throw an error if either component or settingComponent is not a string', async () => { const widgetModule = await import('../../widgetManager.js'); const invalidWidget = { name: 'InvalidWidget', type: 'InvalidType', description: 'This widget has invalid components', settingComponent: 123, // Invalid type component: null, // Invalid type enabled: true, defaultSettings: { theme: 'dark' } }; expect(() => widgetModule.registerWidget(invalidWidget)).toThrow( 'Invalid or unresolvable' ); }); it('should throw an error if either component or settingComponent is unresolvable path', async () => { const widgetModule = await import('../../widgetManager.js'); const invalidWidget = { name: 'InvalidWidget', type: 'InvalidType', description: 'This widget has invalid components', settingComponent: 'InvalidSetting.js', // Unresolvable path component: 'InvalidComponent.js', // Unresolvable path enabled: true, defaultSettings: { theme: 'dark' } }; expect(() => widgetModule.registerWidget(invalidWidget)).toThrow( 'Invalid or unresolvable' ); }); it('should throw an error if registerWidget is called after getAllWidgets', async () => { const widgetModule = await import('../../widgetManager.js'); widgetModule.getAllWidgets(); // This freezes the manager const widget = getValidWidget('NewWidget'); expect(() => widgetModule.registerWidget(widget)).toThrow( 'Widget manager is in a read-only state. No further mutations are allowed. We suggest to register, update, or remove a widget from the bootstrap file.' ); }); it('should throw an error if updateWidget is called after getAllWidgets', async () => { const widgetModule = await import('../../widgetManager.js'); widgetModule.registerWidget(getValidWidget('ExistingWidget')); widgetModule.getAllWidgets(); // This freezes the manager expect(() => widgetModule.updateWidget('ExistingWidget', { description: 'Updated' }) ).toThrow( 'Widget manager is in a read-only state. No further mutations are allowed. We suggest to register, update, or remove a widget from the bootstrap file.' ); }); it('should throw an error if removeWidget is called after getAllWidgets', async () => { const widgetModule = await import('../../widgetManager.js'); widgetModule.registerWidget(getValidWidget('WidgetToRemove')); widgetModule.getAllWidgets(); // This freezes the manager expect(() => widgetModule.removeWidget('WidgetToRemove')).toThrow( 'Widget manager is in a read-only state. No further mutations are allowed. We suggest to register, update, or remove a widget from the bootstrap file.' ); }); it('should allow getWidget after getAllWidgets', async () => { const widgetModule = await import('../../widgetManager.js'); const widget = getValidWidget('ReadWidget'); widgetModule.registerWidget(widget); widgetModule.getAllWidgets(); // This freezes the manager expect(widgetModule.getWidget('ReadWidget')).toEqual(widget); }); it('should allow hasWidget after getAllWidgets', async () => { const widgetModule = await import('../../widgetManager.js'); const widget = getValidWidget('CheckWidget'); widgetModule.registerWidget(widget); widgetModule.getAllWidgets(); // This freezes the manager expect(widgetModule.hasWidget('CheckWidget')).toBe(true); }); it('should allow to updateWidget', async () => { const widgetModule = await import('../../widgetManager.js'); const widget = getValidWidget('UpdateWidget'); widgetModule.registerWidget(widget); const newWidget = { ...widget, description: 'Updated Description', component: 'UpdatedComponent.js' }; widgetModule.updateWidget('UpdateWidget', newWidget); expect(widgetModule.getWidget('UpdateWidget')).toEqual(newWidget); expect(widgetModule.getWidget('UpdateWidget').component).toEqual( 'UpdatedComponent.js' ); expect(widgetModule.getWidget('UpdateWidget').description).toEqual( 'Updated Description' ); }); it('should throw error if trying to updateWidget with non-existing widget', async () => { const widgetModule = await import('../../widgetManager.js'); expect(() => widgetModule.updateWidget('NonExistingWidget', { description: 'Test' }) ).toThrow('Widget not found'); }); it('should thrown an error if trying to update widget after calling getAllWidgets', async () => { const widgetModule = await import('../../widgetManager.js'); widgetModule.registerWidget(getValidWidget('WidgetToUpdate')); widgetModule.getAllWidgets(); // This freezes the manager expect(() => widgetModule.updateWidget('WidgetToUpdate', { description: 'New Desc' }) ).toThrow( 'Widget manager is in a read-only state. No further mutations are allowed. We suggest to register, update, or remove a widget from the bootstrap file.' ); }); it('should allow removing a widget', async () => { const widgetModule = await import('../../widgetManager.js'); const widget = getValidWidget('WidgetToRemove'); widgetModule.registerWidget(widget); widgetModule.removeWidget('WidgetToRemove'); expect(widgetModule.hasWidget('WidgetToRemove')).toBe(false); }); }); }); ================================================ FILE: packages/evershop/src/lib/widget/widgetManager.ts ================================================ import * as fs from 'fs'; import * as path from 'path'; import { Widget } from '../../types/widget.js'; import { warning } from '../log/logger.js'; import { generateComponentKey } from '../util/keyGenerator.js'; /** * Checks if a given path is a valid and resolvable JavaScript file path. * A path is considered valid if it's a string, not empty, exists on the filesystem, * and has a .js extension. * * @param {string | undefined} filePath - The path to check. Can be undefined. * @returns {boolean} True if the path is a resolvable JavaScript file, false otherwise. */ function isValidJsFilePath(filePath: string | undefined): boolean { if (typeof filePath !== 'string' || filePath.trim() === '') { return false; } const resolvedPath = path.resolve(filePath); const fileExtension = path.extname(resolvedPath); try { if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isFile()) { return false; } } catch (e) { return false; } return fileExtension === '.js'; } /** * Checks if the base filename of a given path starts with an uppercase letter. * This is typically used for validating component names, following common conventions. * * @param {string | undefined} filePath - The path to check. Can be undefined. * @returns {boolean} True if the base filename starts with an uppercase letter, false otherwise. */ function isComponentNameUppercase(filePath: string | undefined): boolean { if (typeof filePath !== 'string' || filePath.trim() === '') { return false; } const baseName = path.parse(filePath).name; return ( baseName.length > 0 && baseName[0] === baseName[0].toUpperCase() && !!baseName[0].match(/[A-Z]/) ); } /** * Validates the type of a widget. Only characters and underscores are allowed, no spaces or special characters. * @param type - The type of the widget to validate. * @returns {boolean} True if the type is valid, false otherwise. */ function isValidType(type: string | undefined): boolean { return ( typeof type === 'string' && type.trim() !== '' && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(type) ); } class WidgetManager { /** * @private * A private map to store registered widgets. The key is the widget's unique name, * and the value is the widget object adhering to the Widget interface. */ private widgets: Map = new Map(); /** * @private * A flag indicating whether the widget manager has entered a read-only state. * Once set to true (after `getAllWidgets` is called for the first time), * no further mutations (add, remove, update) are allowed. */ private _isFrozen: boolean = false; /** * Internal helper to check if mutations are allowed. * Throws an error if the manager is in a frozen (read-only) state. * @private * @throws {Error} If a mutation attempt is made after the manager is frozen. */ private _ensureMutable(): void { if (this._isFrozen) { throw new Error( 'Widget manager is in a read-only state. No further mutations are allowed. We suggest to register, update, or remove a widget from the bootstrap file.' ); } } /** * Registers a new widget with the manager. * A widget must have a unique 'type' property. * If a widget with the same type already exists, it will not be registered. * Additionally, `settingComponent` and `component` paths must * be resolvable paths to a JavaScript file, and their base filename must start * with an uppercase letter. * * @param {Widget} widget - The widget object to register. * @returns {boolean} True if the widget was successfully registered, false otherwise. * @throws {Error} If called after the manager has entered a read-only state or on invalid widget data/paths. */ public registerWidget(widget: Widget): boolean { this._ensureMutable(); if (!widget || isValidType(widget.type) === false) { throw new Error( 'Cannot register widget. Widget object must have a valid "type" property.' ); } const widgetType = widget.type; if (this.widgets.has(widgetType)) { warning( `Widget with type "${widgetType}" is already registered. Skipping registration.` ); return false; } if (!isValidJsFilePath(widget.settingComponent)) { throw new Error( `Cannot register widget "${widgetType}". Invalid or unresolvable settingComponent path: "${widget.settingComponent}". Please ensure it's a valid path to an existing JS file.` ); } if (!isComponentNameUppercase(widget.settingComponent)) { throw new Error( `Cannot register widget "${widgetType}". Setting component filename "${ path.parse(widget.settingComponent).name }" must start with an uppercase letter.` ); } if (!isValidJsFilePath(widget.component)) { throw new Error( `Cannot register widget "${widgetType}". Invalid or unresolvable component path: "${widget.component}". Please ensure it's a valid path to an existing JS file.` ); } if (!isComponentNameUppercase(widget.component)) { throw new Error( `Cannot register widget "${widgetType}". Main component filename "${ path.parse(widget.component).name }" must start with an uppercase letter.` ); } this.widgets.set(widgetType, widget); return true; } /** * Updates properties of an existing widget. This is useful for third-party extensions * to modify or "overwrite" parts of a core widget, such as its `settingComponent` or `component`. * * @param {string} widgetType - The type of the widget to update. * @param {Partial} updates - An object containing the properties to update. * @returns {boolean} True if the widget was successfully updated, false otherwise. * @throws {Error} If called after the manager has entered a read-only state or on invalid update data/paths. */ public updateWidget(widgetType: string, updates: Partial): boolean { this._ensureMutable(); if ( !updates || typeof updates !== 'object' || Object.keys(updates).length === 0 ) { throw new Error( `Cannot update widget "${widgetType}". No updates provided or updates object is invalid.` ); } const typeToUpdate = widgetType; const existingWidget = this.widgets.get(typeToUpdate); if (!existingWidget) { throw new Error( `Cannot update widget "${typeToUpdate}". Widget not found.` ); } if (updates.settingComponent !== undefined) { if (!isValidJsFilePath(updates.settingComponent)) { throw new Error( `Cannot update widget "${typeToUpdate}". Invalid or unresolvable new settingComponent path: "${updates.settingComponent}". Please ensure it's a valid path to an existing JS file.` ); } else if (!isComponentNameUppercase(updates.settingComponent)) { throw new Error( `Cannot update widget "${typeToUpdate}". New setting component filename "${ path.parse(updates.settingComponent).name }" must start with an uppercase letter.` ); } else { existingWidget.settingComponent = updates.settingComponent; } } if (updates.component !== undefined) { if (!isValidJsFilePath(updates.component)) { throw new Error( `Error: Cannot update widget "${typeToUpdate}". Invalid or unresolvable new component path: "${updates.component}". Please ensure it's a valid path to an existing JS file.` ); } else if (!isComponentNameUppercase(updates.component)) { throw new Error( `Error: Cannot update widget "${typeToUpdate}". New main component filename "${ path.parse(updates.component).name }" must start with an uppercase letter.` ); } else { existingWidget.component = updates.component; } } for (const key in updates) { if ( Object.prototype.hasOwnProperty.call(updates, key) && key !== 'settingComponent' && key !== 'component' ) { (existingWidget as any)[key] = (updates as any)[key]; } } return true; } /** * Removes a widget from the manager based on its unique type. * * @param {string} widgetType - The type of the widget to remove. * @returns {boolean} True if the widget was successfully removed, false otherwise. * @throws {Error} If called after the manager has entered a read-only state or on invalid widget type. */ public removeWidget(widgetType: string): boolean { this._ensureMutable(); const typeToRemove = widgetType; if (this.widgets.has(typeToRemove)) { this.widgets.delete(typeToRemove); return true; } else { warning(`Widget with type "${typeToRemove}" not found. Cannot remove.`); return false; } } /** * Retrieves a registered widget by its unique type. * * @param {string} widgetType - The type of the widget to retrieve. * @returns {Widget | undefined} The widget object if found, otherwise undefined. */ public getWidget(widgetType: string): Widget | undefined { if (this.widgets.has(widgetType)) { return this.widgets.get(widgetType); } else { warning(`Widget with type "${widgetType}" not found.`); return undefined; } } /** * Retrieves all registered widgets. * Returns a new array containing frozen (immutable) copies of the widget objects. * This method also marks the WidgetManager as 'frozen', preventing any further * calls to mutation methods (register, remove, update). * * @returns {Widget[]} An array containing all registered widget objects. */ public getAllWidgets(): Widget[] { this._isFrozen = true; // Create a new array, and for each widget, create a frozen copy. return Array.from(this.widgets.values()).map((widget) => Object.freeze({ ...widget }) ); } /** * Checks if a widget with the given type is registered. * * @param {string} widgetType - The type of the widget to check. * @returns {boolean} True if the widget is registered, false otherwise. */ public hasWidget(widgetType: string): boolean { return this.widgets.has(widgetType); } } const widgetManager = new WidgetManager(); /** * Retrieves all registered widgets. This function returns a new array containing * all widgets, each with its `settingComponentKey` and `componentKey` properties * generated using the `generateComponentKey` function. * Calling this function will also freeze the widget manager, preventing any further mutations (register, remove, update). * @returns {Widget[]} An array of all registered widgets. */ export function getAllWidgets(): Widget[] { const allWidgets = widgetManager.getAllWidgets(); return allWidgets.map((widget) => { return { ...widget, settingComponentKey: generateComponentKey(widget.settingComponent), componentKey: generateComponentKey(widget.component) }; }); } /** * Retrieves all enabled widgets. An enabled widget is one that has its `enabled` property set to true. * This function returns a new array containing only the widgets that are enabled. Calling this function * will also freeze the widget manager, preventing any further mutations (register, remove, update). * @returns {Widget[]} An array of enabled widgets. */ export function getEnabledWidgets(): Widget[] { const allWidgets = widgetManager.getAllWidgets(); return allWidgets .filter((widget) => widget.enabled) .map((widget) => { return { ...widget, settingComponentKey: generateComponentKey(widget.settingComponent), componentKey: generateComponentKey(widget.component) }; }); } /** * Registers a new widget. This function is intended to be called during the * bootstrap phase of the application, before the widget manager is frozen. * @param widget - The widget object to register. * @returns True if the widget was successfully registered, false otherwise. * @throws Error if the widget is invalid or if the manager is in a read-only state. */ export function registerWidget(widget: Widget): boolean { return widgetManager.registerWidget(widget); } /** * Updates properties of an existing widget. This is useful for third-party extensions * to modify or "overwrite" parts of a core widget, such as its `settingComponent` or `component`. * @param widgetType - The type of the widget to update. * @param updates - An object containing the properties to update. * @returns True if the widget was successfully updated, false otherwise. */ export function updateWidget( widgetType: string, updates: Partial ): boolean { return widgetManager.updateWidget(widgetType, updates); } /** * Removes a widget. This function supposed to be called from the bootstrap * phase of the application, before the widget manager is frozen. * @param widgetName - The name of the widget to remove. * @returns True if the widget was successfully removed, false otherwise. */ export function removeWidget(widgetName: string): boolean { return widgetManager.removeWidget(widgetName); } /** * Retrieves a widget by its type. * @param widgetType - The type of the widget to retrieve. * @returns The widget if found, undefined otherwise. */ export function getWidget(widgetType: string): Widget | undefined { return widgetManager.getWidget(widgetType); } /** * Checks if a widget with the given type is registered. * @param widgetType - The type of the widget to check. * @returns True if the widget is registered, false otherwise. */ export function hasWidget(widgetType: string): boolean { return widgetManager.hasWidget(widgetType); } ================================================ FILE: packages/evershop/src/modules/auth/api/getUserToken/[context]bodyParser[auth].ts ================================================ import bodyParser from 'body-parser'; export default (request, response, next) => { bodyParser.json({ inflate: false })(request, response, next); }; ================================================ FILE: packages/evershop/src/modules/auth/api/getUserToken/generateToken.ts ================================================ import { INTERNAL_SERVER_ERROR, INVALID_PAYLOAD, OK } from '../../../../lib/util/httpStatus.js'; import { generateToken, generateRefreshToken, TOKEN_TYPES } from '../../../../lib/util/jwt.js'; import { CurrentUser, EvershopRequest } from '../../../../types/request.js'; import { EvershopResponse } from '../../../../types/response.js'; export default async ( request: EvershopRequest, response: EvershopResponse, next ) => { try { const message = 'Invalid email or password'; const { body } = request; const { email, password } = body; await request.loginUserWithEmail(email, password, (error) => { if (error) { response.status(INTERNAL_SERVER_ERROR); response.json({ error: { status: INTERNAL_SERVER_ERROR, message } }); return; } }); response.status(OK); const accessToken = generateToken( { user: request.locals.user as CurrentUser }, TOKEN_TYPES.ADMIN ); const refreshToken = generateRefreshToken( { user: request.locals.user as CurrentUser }, TOKEN_TYPES.ADMIN ); response.json({ data: { accessToken, refreshToken } }); } catch (error) { response.status(INVALID_PAYLOAD).json({ error: { message: error.message, status: INVALID_PAYLOAD } }); } }; ================================================ FILE: packages/evershop/src/modules/auth/api/getUserToken/payloadSchema.json ================================================ { "type": "object", "properties": { "email": { "type": "string", "format": "email" }, "password": { "type": "string" }, "full_name": { "type": "string" } }, "additionalProperties": true } ================================================ FILE: packages/evershop/src/modules/auth/api/getUserToken/route.json ================================================ { "methods": ["POST"], "path": "/user/tokens", "access": "public" } ================================================ FILE: packages/evershop/src/modules/auth/api/global/[context]getCurrentUser.ts ================================================ import util from 'util'; import { select } from '@evershop/postgres-query-builder'; import sessionStorage from 'connect-pg-simple'; import session from 'express-session'; import { pool } from '../../../../lib/postgres/connection.js'; import { EvershopRequest } from '../../../../types/request.js'; import { setContextValue } from '../../../graphql/services/contextHelper.js'; import { getAdminSessionCookieName } from '../../services/getAdminSessionCookieName.js'; /** * This is the session based authentication middleware. * We do not implement session middleware on API routes, instead we only load the session from the database and set the user in the context. * @param {*} request * @param {*} response * @param {*} next * @returns */ export default async (request: EvershopRequest, response, next) => { // Check if the user is authenticated, if yes we assume previous authentication middleware has set the user in the context let currentAdminUser = request.getCurrentUser(); if (!currentAdminUser) { try { // Get the sesionID cookies const cookies = request.signedCookies; const adminSessionCookieName = getAdminSessionCookieName(); // Check if the sessionID cookie is present const sessionID = cookies[adminSessionCookieName]; if (sessionID) { const storage = new (sessionStorage(session))({ pool }); // Load the session using session storage const getSession = util.promisify(storage.get).bind(storage); const adminSessionData = await getSession(sessionID); if (adminSessionData) { // Set the user in the context currentAdminUser = await select() .from('admin_user') .where('admin_user_id', '=', adminSessionData.userID) .and('status', '=', 1) .load(pool); if (currentAdminUser) { // Delete the password field if present (cast to any to avoid TS error) if ('password' in currentAdminUser) { delete (currentAdminUser as any).password; } request.locals.user = currentAdminUser; setContextValue(request, 'user', currentAdminUser); } } } } catch (e) { // Do nothing, the user is not logged in } } next(); }; ================================================ FILE: packages/evershop/src/modules/auth/api/global/[context]jwtUserAuth[getCurrentUser].ts ================================================ import { UNAUTHORIZED } from '../../../../lib/util/httpStatus.js'; import { decodeToken, TOKEN_TYPES, verifyToken } from '../../../../lib/util/jwt.js'; import { EvershopRequest } from '../../../../types/request.js'; export default (request: EvershopRequest, response, next) => { try { // Get token from Authorization header const authHeader = request.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return next(); } const token = authHeader.substring(7); const decodedWithoutVerification = decodeToken(token); if ( !decodedWithoutVerification || decodedWithoutVerification.tokenType !== TOKEN_TYPES.ADMIN ) { // If token type is not admin, skip processing return next(); } // Verify token const decoded = verifyToken(token, TOKEN_TYPES.ADMIN); // Attach user info to request request.locals = request.locals || {}; request.locals.user = decoded.user; return next(); } catch (error) { response.status(UNAUTHORIZED); return response.json({ error: { status: UNAUTHORIZED, message: error.message || 'Invalid token' } }); } }; ================================================ FILE: packages/evershop/src/modules/auth/api/global/[getCurrentUser]auth.ts ================================================ import { UNAUTHORIZED } from '../../../../lib/util/httpStatus.js'; import { EvershopRequest } from '../../../../types/request.js'; /** * This is the session based authentication middleware. * We do not implement session middleware on API routes, instead we only load the session from the database and set the user in the context. * @param {*} request * @param {*} response * @param {*} next * @returns */ export default async (request: EvershopRequest, response, next) => { // Get the current route const { currentRoute } = request; const currentAdminUser = request.getCurrentUser(); // If the current route is public, continue to the next middleware // Missing access property means private if (currentRoute?.access === 'public') { next(); return; } if (!currentAdminUser?.uuid) { // Response with 401 status code response.status(UNAUTHORIZED); response.json({ error: { status: UNAUTHORIZED, message: 'Unauthorized' } }); } else { // Get user roles const userRoles = currentAdminUser.roles || '*'; if (userRoles === '*') { next(); } else { const roles = userRoles.split(','); if (roles.includes(currentRoute.id)) { next(); } else { response.status(UNAUTHORIZED); response.json({ error: { status: UNAUTHORIZED, message: 'Unauthorized' } }); } } } }; ================================================ FILE: packages/evershop/src/modules/auth/api/global/[getCurrentUser]demoAccountBlocking[auth].ts ================================================ import { getEnv } from '../../../../lib/util/getEnv.js'; import { UNAUTHORIZED } from '../../../../lib/util/httpStatus.js'; import { EvershopRequest } from '../../../../types/request.js'; export default (request: EvershopRequest, response, next) => { const { currentRoute } = request; if ( request.method === 'GET' || currentRoute?.id === 'adminGraphql' || currentRoute?.access === 'public' ) { next(); } else { const user = request.getCurrentUser(); const currentUserEmail = user?.email; const demoUserEmails = getEnv('DEMO_USER_EMAILS', '').split(','); if ( user && demoUserEmails && demoUserEmails.includes(currentUserEmail || '') ) { response.status(UNAUTHORIZED).json({ error: { status: UNAUTHORIZED, message: 'The demo account is not allowed to make changes' } }); } else { next(); } } }; ================================================ FILE: packages/evershop/src/modules/auth/api/refreshUserToken/[context]bodyParser[auth].ts ================================================ import bodyParser from 'body-parser'; export default (request, response, next) => { bodyParser.json({ inflate: false })(request, response, next); }; ================================================ FILE: packages/evershop/src/modules/auth/api/refreshUserToken/payloadSchema.json ================================================ { "type": "object", "properties": { "refreshToken": { "type": "string", "errorMessage": { "type": "Refresh token must be a string" } } }, "required": ["refreshToken"], "errorMessage": { "required": { "refreshToken": "Refresh token is required" } } } ================================================ FILE: packages/evershop/src/modules/auth/api/refreshUserToken/refreshToken.ts ================================================ import { select } from '@evershop/postgres-query-builder'; import { pool } from '../../../../lib/postgres/connection.js'; import { INVALID_PAYLOAD, UNAUTHORIZED } from '../../../../lib/util/httpStatus.js'; import { generateToken, TOKEN_TYPES, verifyRefreshToken } from '../../../../lib/util/jwt.js'; export default async (request, response, next) => { const { refreshToken } = request.body; if (!refreshToken) { return response.status(INVALID_PAYLOAD).json({ error: { status: INVALID_PAYLOAD, message: 'Refresh token is required' } }); } try { // Verify refresh token const decoded = verifyRefreshToken(refreshToken, TOKEN_TYPES.ADMIN); // Get fresh admin user data const adminUser = await select() .from('admin_user') .where('admin_user_id', '=', decoded.user.admin_user_id) .and('status', '=', 1) .load(pool); if (!adminUser) { return response.status(UNAUTHORIZED).json({ error: { status: UNAUTHORIZED, message: 'Admin user not found or inactive' } }); } // Generate new access token const payload = decoded.user; const newAccessToken = generateToken( { user: payload }, TOKEN_TYPES.ADMIN ); return response.json({ success: true, data: { accessToken: newAccessToken } }); } catch (error) { return response.status(UNAUTHORIZED).json({ error: { status: UNAUTHORIZED, message: error.message || 'Invalid refresh token' } }); } }; ================================================ FILE: packages/evershop/src/modules/auth/api/refreshUserToken/route.json ================================================ { "methods": ["POST"], "path": "/user/token/refresh", "access": "public" } ================================================ FILE: packages/evershop/src/modules/auth/bootstrap.js ================================================ import { request } from 'express'; import { hookable } from '../../lib/util/hookable.js'; import { loginUserWithEmail } from './services/loginUserWithEmail.js'; import { logoutUser } from './services/logoutUser.js'; export default () => { request.loginUserWithEmail = async function login(email, password, callback) { await hookable(loginUserWithEmail.bind(this))(email, password); if (this.session) { this.session.save(callback); } }; request.logoutUser = function logout(callback) { hookable(logoutUser.bind(this))(); if (this.session) { this.session.save(callback); } }; request.isUserLoggedIn = function isUserLoggedIn() { return !!this.session.userID; }; request.getCurrentUser = function getCurrentUser() { return this.locals.user; }; }; ================================================ FILE: packages/evershop/src/modules/auth/graphql/types/AdminUser/AdminUser.admin.graphql ================================================ """ Retrieves a single admin user by ID """ type AdminUser { adminUserId: Int! uuid: String! status: Int! email: String! fullName: String! } """ Retrieves a collection of admin users """ type AdminUserCollection { items: [AdminUser] currentPage: Int! total: Int! currentFilters: [Filter] } extend type Query { adminUser(id: Int): AdminUser currentAdminUser: AdminUser adminUsers(filters: [FilterInput]): AdminUserCollection } ================================================ FILE: packages/evershop/src/modules/auth/graphql/types/AdminUser/AdminUser.admin.resolvers.js ================================================ import { select } from '@evershop/postgres-query-builder'; import { camelCase } from '../../../../../lib/util/camelCase.js'; export default { Query: { adminUser: async (root, { id }, { pool }) => { const query = select().from('admin_user'); query.where('admin_user_id', '=', id); const adminUser = await query.load(pool); return adminUser ? camelCase(adminUser) : null; }, currentAdminUser: (root, args, { user }) => (user ? camelCase(user) : null), adminUsers: async (_, { filters = [] }, { pool }) => { const query = select().from('admin_user'); const currentFilters = []; // Attribute filters filters.forEach((filter) => { if (filter.key === 'full_name') { query.andWhere('admin_user.full_name', 'LIKE', `%${filter.value}%`); currentFilters.push({ key: 'full_name', operation: 'eq', value: filter.value }); } if (filter.key === 'status') { query.andWhere('admin_user.status', '=', filter.value); currentFilters.push({ key: 'status', operation: 'eq', value: filter.value }); } }); const sortBy = filters.find((f) => f.key === 'sortBy'); const sortOrder = filters.find( (f) => f.key === 'sortOrder' && ['ASC', 'DESC'].includes(f.value) ) || { value: 'ASC' }; if (sortBy && sortBy.value === 'full_name') { query.orderBy('admin_user.full_name', sortOrder.value); currentFilters.push({ key: 'sortBy', operation: 'eq', value: sortBy.value }); } else { query.orderBy('admin_user.admin_user_id', 'DESC'); } if (sortOrder.key) { currentFilters.push({ key: 'sortOrder', operation: 'eq', value: sortOrder.value }); } // Clone the main query for getting total right before doing the paging const cloneQuery = query.clone(); cloneQuery.select('COUNT(admin_user.admin_user_id)', 'total'); cloneQuery.removeOrderBy(); // Paging const page = filters.find((f) => f.key === 'page') || { value: 1 }; const limit = filters.find((f) => f.key === 'limit' && f.value > 0) || { value: 20 }; // TODO: Get from the config currentFilters.push({ key: 'page', operation: 'eq', value: page.value }); currentFilters.push({ key: 'limit', operation: 'eq', value: limit.value }); query.limit( (page.value - 1) * parseInt(limit.value, 10), parseInt(limit.value, 10) ); return { items: (await query.execute(pool)).map((row) => camelCase(row)), total: (await cloneQuery.load(pool)).total, currentFilters }; } } }; ================================================ FILE: packages/evershop/src/modules/auth/migration/Version-1.0.0.js ================================================ import { execute } from '@evershop/postgres-query-builder'; export default async (connection) => { await execute( connection, `CREATE TABLE IF NOT EXISTS "admin_user" ( "admin_user_id" INT GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1) PRIMARY KEY, "uuid" UUID NOT NULL DEFAULT gen_random_uuid (), "status" boolean NOT NULL DEFAULT TRUE, "email" varchar NOT NULL, "password" varchar NOT NULL, "full_name" varchar DEFAULT NULL, "created_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "ADMIN_USER_EMAIL_UNIQUE" UNIQUE ("email"), CONSTRAINT "ADMIN_USER_UUID_UNIQUE" UNIQUE ("uuid") );` ); await execute( connection, `CREATE TABLE "user_token_secret" ( "user_token_secret_id" INT GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1) PRIMARY KEY, "sid" UUID NOT NULL DEFAULT gen_random_uuid (), "user_id" varchar NOT NULL, "secret" varchar NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "USER_TOKEN_SID_UNIQUE" UNIQUE ("sid"), CONSTRAINT "USER_TOKEN_SECRET_UNIQUE" UNIQUE ("secret") );` ); }; ================================================ FILE: packages/evershop/src/modules/auth/migration/Version-1.0.1.js ================================================ import { execute } from '@evershop/postgres-query-builder'; export default async (connection) => { // Remove user_token_secret table await execute(connection, `DROP TABLE IF EXISTS user_token_secret;`); // Create a session table following the `connect-pg-simple` package await execute( connection, `CREATE TABLE IF NOT EXISTS session ( sid varchar NOT NULL COLLATE "default", sess json NOT NULL, expire timestamp(6) NOT NULL ) WITH (OIDS=FALSE); ALTER TABLE session ADD CONSTRAINT "SESSION_PKEY" PRIMARY KEY ("sid") NOT DEFERRABLE INITIALLY IMMEDIATE;` ); await execute( connection, `CREATE INDEX "IDX_SESSION_EXPIRE" ON "session" ("expire");` ); }; ================================================ FILE: packages/evershop/src/modules/auth/pages/admin/adminLogin/LoginForm.scss ================================================ body.adminLogin { .content-wrapper { margin-top: 0; } .admin-navigation { display: none; } .main-content { margin-left: 0; } .main-content-inner { display: flex; justify-content: center; justify-items: center; align-items: center; } .admin-login-form { width: 468px; max-width: 80%; background-color: white; border-radius: 5px; padding: 2.5rem; -webkit-box-shadow: 6px 12px 60px rgb(0 0 0 / 20%); box-shadow: 6px 12px 60px rgb(0 0 0 / 20%); h1 { font-size: 1.875rem; margin-bottom: 0.625rem; font-weight: 400; text-align: center; } .form-submit-button { button { width: 100%; padding: 0.75rem 1rem; } } .form-field-container { label { color: #666; } } input[type='email'], input[type='password'] { padding: 0.5rem 0.75rem; } } } ================================================ FILE: packages/evershop/src/modules/auth/pages/admin/adminLogin/LoginForm.tsx ================================================ import React from 'react'; import './LoginForm.scss'; import Area from '@components/common/Area.js'; import { EmailField } from '@components/common/form/EmailField.js'; import { Form, useFormContext } from '@components/common/form/Form.js'; import { PasswordField } from '@components/common/form/PasswordField.js'; import { Button } from '@components/common/ui/Button.js'; import { LockKeyhole, Mail } from 'lucide-react'; interface LoginFormProps { authUrl: string; dashboardUrl: string; } const SubmitButton: React.FC = () => { const { formState: { isSubmitting } } = useFormContext(); return (
    ); }; export default function LoginForm({ authUrl, dashboardUrl }: LoginFormProps) { const [error, setError] = React.useState(null); const onSuccess = (response) => { if (!response.error) { window.location.href = dashboardUrl; } else { setError(response.error.message); } }; return (
    {error &&
    {error}
    }
    } label="Email" name="email" placeholder="Email" required validation={{ required: 'Email is required' }} /> ) }, sortOrder: 10 }, { component: { default: ( } label="Password" name="password" placeholder="Password" required validation={{ required: 'Password is required' }} showToggle /> ) }, sortOrder: 20 }, { component: { default: }, sortOrder: 30 } ]} />
    ); } export const layout = { areaId: 'content', sortOrder: 10 }; export const query = ` query Query { authUrl: url(routeId: "adminLoginJson") dashboardUrl: url(routeId: "dashboard") } `; ================================================ FILE: packages/evershop/src/modules/auth/pages/admin/adminLogin/index.ts ================================================ import { buildUrl } from '../../../../../lib/router/buildUrl.js'; import { setPageMetaInfo } from '../../../../cms/services/pageMetaInfo.js'; export default (request, response, next) => { // Check if the user is logged in const user = request.getCurrentUser(); if (user) { // Redirect to admin dashboard response.redirect(buildUrl('dashboard')); } else { setPageMetaInfo(request, { title: 'Admin Login', description: 'Admin Login' }); next(); } }; ================================================ FILE: packages/evershop/src/modules/auth/pages/admin/adminLogin/route.json ================================================ { "methods": ["GET"], "path": "/login" } ================================================ FILE: packages/evershop/src/modules/auth/pages/admin/adminLoginJson/[bodyParser]logIn.js ================================================ import { translate } from '../../../../../lib/locale/translate/translate.js'; import { INTERNAL_SERVER_ERROR, INVALID_PAYLOAD, OK } from '../../../../../lib/util/httpStatus.js'; export default async (request, response, next) => { try { const message = translate('Invalid email or password'); const { body } = request; const { email, password } = body; await request.loginUserWithEmail(email, password, (error) => { if (error) { response.status(INTERNAL_SERVER_ERROR); response.json({ error: { status: INTERNAL_SERVER_ERROR, message } }); } else { response.status(OK); response.$body = { data: { sid: request.sessionID } }; next(); } }); } catch (error) { response.status(INVALID_PAYLOAD).json({ error: { message: error.message, status: INVALID_PAYLOAD } }); } }; ================================================ FILE: packages/evershop/src/modules/auth/pages/admin/adminLoginJson/payloadSchema.json ================================================ { "type": "object", "properties": { "email": { "type": "string", "format": "email" }, "password": { "type": ["string"] } }, "required": ["email", "password"], "additionalProperties": true, "errorMessage": { "properties": { "email": "Email or password is invalid", "password": "Email or password is invalid" } } } ================================================ FILE: packages/evershop/src/modules/auth/pages/admin/adminLoginJson/route.json ================================================ { "name": "Admin User Login", "description": "Admin User Login", "methods": ["POST"], "path": "/user/login" } ================================================ FILE: packages/evershop/src/modules/auth/pages/admin/adminLogoutJson/logout.js ================================================ import { INTERNAL_SERVER_ERROR, OK } from '../../../../../lib/util/httpStatus.js'; export default (request, response, next) => { try { request.logoutUser((error) => { if (error) { response.status(INTERNAL_SERVER_ERROR); response.json({ error: { status: INTERNAL_SERVER_ERROR, message: error.message } }); } else { response.status(OK); response.$body = { data: {} }; next(); } }); } catch (error) { response.status(INTERNAL_SERVER_ERROR); response.json({ error: { status: INTERNAL_SERVER_ERROR, message: error.message } }); } }; ================================================ FILE: packages/evershop/src/modules/auth/pages/admin/adminLogoutJson/route.json ================================================ { "methods": ["GET", "POST"], "path": "/user/logout" } ================================================ FILE: packages/evershop/src/modules/auth/pages/admin/all/AdminUser.jsx ================================================ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@components/common/ui/DropdownMenu.js'; import { LogOut } from 'lucide-react'; import PropTypes from 'prop-types'; import React from 'react'; import { toast } from 'react-toastify'; export default function AdminUser({ adminUser, logoutUrl, loginPage }) { const logout = async () => { const response = await fetch(logoutUrl, { method: 'GET', headers: { 'Content-Type': 'application/json' } }); if (response.status === 200) { window.location.href = loginPage; } else { toast.error('Logout failed'); } }; if (!adminUser) { return null; } const { fullName } = adminUser; return (
    Hello {fullName}! { e.preventDefault(); logout(); }} >
    Logout
    ); } AdminUser.propTypes = { adminUser: PropTypes.shape({ email: PropTypes.string.isRequired, fullName: PropTypes.string.isRequired }), loginPage: PropTypes.string.isRequired, logoutUrl: PropTypes.string.isRequired }; AdminUser.defaultProps = { adminUser: null }; export const layout = { areaId: 'header', sortOrder: 50 }; export const query = ` query Query { adminUser: currentAdminUser { adminUserId fullName email }, logoutUrl: url(routeId: "adminLogoutJson"), loginPage: url(routeId: "adminLogin") } `; ================================================ FILE: packages/evershop/src/modules/auth/pages/admin/all/[context]auth.js ================================================ import { select } from '@evershop/postgres-query-builder'; import { pool } from '../../../../../lib/postgres/connection.js'; import { buildUrl } from '../../../../../lib/router/buildUrl.js'; export default async (request, response, next) => { const { userID } = request.session; // Load the user from the database const user = await select() .from('admin_user') .where('admin_user_id', '=', userID) .and('status', '=', 1) .load(pool); if (!user) { // The user may not be logged in, or the account may be disabled // Logout the user request.logoutUser(() => { // Check if current route is adminLogin if ( request.currentRoute.id === 'adminLogin' || request.currentRoute.id === 'adminLoginJson' ) { next(); } else { response.redirect(buildUrl('adminLogin')); } }); } else { // Delete the password field delete user.password; request.locals.user = user; next(); } }; ================================================ FILE: packages/evershop/src/modules/auth/services/getAdminSessionCookieName.ts ================================================ import { getConfig } from '../../../lib/util/getConfig.js'; export const getAdminSessionCookieName = (): string => getConfig('system.session.adminCookieName', 'asid'); ================================================ FILE: packages/evershop/src/modules/auth/services/getCookieSecret.ts ================================================ import { getConfig } from '../../../lib/util/getConfig.js'; export const getCookieSecret = (): string => getConfig('system.session.cookieSecret', 'keyboard cat'); ================================================ FILE: packages/evershop/src/modules/auth/services/getFrontStoreSessionCookieName.ts ================================================ import { getConfig } from '../../../lib/util/getConfig.js'; export const getFrontStoreSessionCookieName = (): string => getConfig('system.session.cookieName', 'sid'); ================================================ FILE: packages/evershop/src/modules/auth/services/getSessionConfig.ts ================================================ import sessionStorage from 'connect-pg-simple'; import session from 'express-session'; import { pool } from '../../../lib/postgres/connection.js'; import { getConfig } from '../../../lib/util/getConfig.js'; export const getSessionConfig = (cookieSecret): session.SessionOptions => { const sess = { store: new (sessionStorage(session))({ pool }), secret: cookieSecret, cookie: { maxAge: 24 * 60 * 60 * 1000 }, resave: getConfig('system.session.resave', false), saveUninitialized: getConfig('system.session.saveUninitialized', false) }; return sess; }; ================================================ FILE: packages/evershop/src/modules/auth/services/loginUserWithEmail.ts ================================================ import { select } from '@evershop/postgres-query-builder'; import { pool } from '../../../lib/postgres/connection.js'; import { comparePassword } from '../../../lib/util/passwordHelper.js'; /** * This function will login the admin user with email and password. This function must be accessed from the request object (request.loginUserWithEmail(email, password, callback)) * @param {string} email * @param {string} password */ async function loginUserWithEmail( email: string, password: string ): Promise { // Escape the email to prevent SQL injection const userEmail = email.replace(/%/g, '\\%'); const user = await select() .from('admin_user') .where('email', 'ILIKE', userEmail) .and('status', '=', 1) .load(pool); const result = comparePassword(password, user ? user.password : ''); if (!user || !result) { throw new Error('Invalid email or password'); } if (this.session) { this.session.userID = user.admin_user_id; } // Delete the password field delete user.password; // Save the user in the request this.locals.user = user; } export { loginUserWithEmail }; ================================================ FILE: packages/evershop/src/modules/auth/services/logoutUser.ts ================================================ /** * Logout a current user. This function must be accessed from the request object (request.logoutUser(callback)) */ export function logoutUser() { this.session.userID = undefined; this.locals.user = undefined; } ================================================ FILE: packages/evershop/src/modules/base/api/global/[apiResponse]apiErrorHandler.ts ================================================ import { error } from '../../../../lib/log/logger.js'; import { INTERNAL_SERVER_ERROR } from '../../../../lib/util/httpStatus.js'; import isDevelopmentMode from '../../../../lib/util/isDevelopmentMode.js'; import { EvershopRequest } from '../../../../types/request.js'; import { EvershopResponse } from '../../../../types/response.js'; export default async ( err, request: EvershopRequest, response: EvershopResponse, next ) => { if (isDevelopmentMode() || process.argv.includes('--debug')) { error(err); } // Check if the header is already sent or not. if (response.headersSent) { // TODO: Write a log message or next(error)?. } else { let status = INTERNAL_SERVER_ERROR; if (!response.statusCode || response.statusCode === 200) { response.status(status); } else { status = response.statusCode; } response.json({ error: { status, message: err.message } }); } }; ================================================ FILE: packages/evershop/src/modules/base/api/global/[auth]apiResponse[apiErrorHandler].ts ================================================ import isErrorHandlerTriggered from '../../../../lib/middleware/isErrorHandlerTriggered.js'; import { EvershopRequest } from '../../../../types/request.js'; import { EvershopResponse } from '../../../../types/response.js'; export default async ( request: EvershopRequest, response: EvershopResponse, next ) => { try { /** If a rejected middleware called next(error) without throwing an error */ if (isErrorHandlerTriggered(response)) { return; } else { response.json(response.$body || {}); } } catch (error) { if (!isErrorHandlerTriggered(response)) { next(error); } else { // Do nothing here since the next(error) is already called // when the error is thrown on each middleware } } }; ================================================ FILE: packages/evershop/src/modules/base/api/global/[auth]payloadValidate.ts ================================================ import { INVALID_PAYLOAD } from '../../../../lib/util/httpStatus.js'; import { getValueSync } from '../../../../lib/util/registry.js'; import { EvershopRequest } from '../../../../types/request.js'; import { EvershopResponse } from '../../../../types/response.js'; import { getAjv } from '../../services/getAjv.js'; import markSkipEscape from '../../services/markSkipEscape.js'; // Initialize the ajv instance const ajv = getAjv(); // Define a custom keyword for html escape ajv.addKeyword({ keyword: 'skipEscape', modifying: true, compile(sch, parentSchema) { return (data, t) => { if (sch === true) { // Mark the data as skip escape markSkipEscape(t.rootData, t.instancePath); return true; } else { return true; } }; } }); export default (request: EvershopRequest, response: EvershopResponse, next) => { // Get the current route const { currentRoute } = request; // Get the payload schema const payloadSchema = getValueSync( 'payloadSchema', currentRoute.payloadSchema, { route: currentRoute } ); if (!payloadSchema) { next(); } else { const validate = ajv.compile(payloadSchema); const valid = validate(request.body); if (valid) { next(); } else { response.status(INVALID_PAYLOAD); response.json({ error: { status: INVALID_PAYLOAD, message: validate.errors[0].message } }); } } }; ================================================ FILE: packages/evershop/src/modules/base/api/global/[payloadValidate]escapeHtml.ts ================================================ import { EvershopRequest } from '../../../../types/request.js'; import { EvershopResponse } from '../../../../types/response.js'; import escapePayload from '../../services/escapePayload.js'; export default (request: EvershopRequest, response: EvershopResponse, next) => { if (request.method === 'GET') { next(); } else { escapePayload(request.body); next(); } }; ================================================ FILE: packages/evershop/src/modules/base/api/global/context.js ================================================ import { pool } from '../../../../lib/postgres/connection.js'; import { getBaseUrl } from '../../../../lib/util/getBaseUrl.js'; import { getConfig } from '../../../../lib/util/getConfig.js'; import { hasContextValue, setContextValue } from '../../../graphql/services/contextHelper.js'; export default (request, response) => { response.context = {}; /** Some default context value */ if (!hasContextValue(request, 'pool')) { setContextValue(request.app, 'pool', pool); } const homeUrl = getBaseUrl(); setContextValue(request.app, 'homeUrl', homeUrl); setContextValue(request, 'currentUrl', `${homeUrl}${request.originalUrl}`); setContextValue(request, 'baseUrl', request.baseUrl); setContextValue(request, 'body', request.body); setContextValue(request, 'cookies', request.cookies); setContextValue(request, 'fresh', request.fresh); setContextValue(request.app, 'hostname', request.hostname); setContextValue(request, 'ip', request.ip); setContextValue(request, 'ips', request.ips); setContextValue(request, 'method', request.method); setContextValue(request, 'originalUrl', request.originalUrl); setContextValue(request, 'params', request.params); setContextValue(request, 'path', request.path); setContextValue(request, 'protocol', request.protocol); setContextValue(request, 'query', request.query); setContextValue(request, 'route', request.route); setContextValue(request, 'secure', request.secure); setContextValue(request, 'signedCookies', request.signedCookies); setContextValue(request, 'stale', request.stale); setContextValue(request, 'subdomains', request.subdomains); setContextValue(request, 'xhr', request.xhr); setContextValue(request, 'sid', request.sessionID); }; ================================================ FILE: packages/evershop/src/modules/base/bootstrap.js ================================================ import { loadCsv } from '../../lib/locale/translate/translate.js'; import { merge } from '../../lib/util/merge.js'; import { addProcessor } from '../../lib/util/registry.js'; export default async () => { await loadCsv(); addProcessor('configurationSchema', (schema) => { merge(schema, { properties: { shop: { type: 'object', properties: { homeUrl: { type: 'string', format: 'uri' }, weightUnit: { type: 'string' }, currency: { type: 'string' }, language: { type: 'string' }, timezone: { type: 'string' } } }, system: { type: 'object', properties: { extensions: { type: 'array', items: { type: 'object', properties: { name: { type: 'string' }, resolve: { type: 'string' }, enabled: { type: 'boolean' }, priority: { type: 'number' } }, required: ['name', 'enabled', 'resolve'] } }, theme: { type: 'string', required: ['name'] }, session: { type: 'object', properties: { cookieSecret: { type: 'string' }, cookieName: { type: 'string' }, maxAge: { type: 'number' }, reSave: { type: 'boolean' }, saveUninitialized: { type: 'boolean' } } } } } } }); return schema; }); }; ================================================ FILE: packages/evershop/src/modules/base/graphql/types/Country/Country.graphql ================================================ """ The `Country` type represents a country. """ type Country { name: String! code: String! provinces: [Province] } extend type Query { countries(countries: [String]): [Country] allowedCountries: [Country] } ================================================ FILE: packages/evershop/src/modules/base/graphql/types/Country/Country.resolvers.js ================================================ import { select } from '@evershop/postgres-query-builder'; import { countries } from '../../../../../lib/locale/countries.js'; import { provinces } from '../../../../../lib/locale/provinces.js'; import { pool } from '../../../../../lib/postgres/connection.js'; export default { Query: { countries: (_, argument) => { const list = argument?.countries || []; if (list.length === 0) { return countries; } else { return countries.filter((c) => list.includes(c.code)); } }, allowedCountries: async () => { const allowedCountries = await select('country') .from('shipping_zone') .execute(pool); return countries.filter((c) => allowedCountries.find((p) => p.country === c.code) ); } }, Country: { name: (country) => { if (country.name) { return country.name; } else { const c = countries.find((p) => p.code === country); return c.name; } }, code: (country) => { if (country.code) { return country.code; } else { return country; } }, provinces: (country) => provinces.filter((p) => p.countryCode === country.code) } }; ================================================ FILE: packages/evershop/src/modules/base/graphql/types/Currency/Currency.graphql ================================================ """ A currency """ type Currency { name: String! code: String! } extend type Query { currencies: [Currency]! } ================================================ FILE: packages/evershop/src/modules/base/graphql/types/Currency/Currency.resolvers.js ================================================ import { currencies } from '../../../../../lib/locale/currencies.js'; export default { Query: { currencies: () => currencies } }; ================================================ FILE: packages/evershop/src/modules/base/graphql/types/DateTime/DateTime.graphql ================================================ """ A DateTime is a string with a timezone. """ type DateTime { value: String timezone: String text(format: String): String } ================================================ FILE: packages/evershop/src/modules/base/graphql/types/DateTime/DateTime.resolvers.js ================================================ import { DateTime } from 'luxon'; import { getConfig } from '../../../../../lib/util/getConfig.js'; export default { DateTime: { value: (dateTime) => dateTime, timezone: async () => { const timeZone = getConfig('shop.timezone', 'UTC'); return timeZone; }, text: async (value, { format = 'yyyy-LL-dd' }) => { if (!DateTime.fromJSDate(value).isValid) { return null; } const timeZone = getConfig('shop.timezone', 'UTC'); const language = getConfig('shop.language', 'en'); const date = DateTime.fromJSDate(value, { zone: timeZone }) .setLocale(language) .setZone(timeZone) .toFormat(format); return date; } } }; ================================================ FILE: packages/evershop/src/modules/base/graphql/types/Province/Province.graphql ================================================ """ The `Province` type represents a province/state. """ type Province { name: String! code: String! countryCode: String! } extend type Query { provinces(countries: [String]): [Province] } ================================================ FILE: packages/evershop/src/modules/base/graphql/types/Province/Province.resolvers.js ================================================ import { provinces } from '../../../../../lib/locale/provinces.js'; export default { Query: { provinces: (_, { countries = [] }) => { if (countries.length === 0) { return provinces; } else { return provinces.filter((p) => countries.includes(p.countryCode)); } } }, Province: { name: (province) => { if (province.name) { return province.name; } else { const p = provinces.find((pr) => pr.code === province); return p?.name || 'INVALID_PROVINCE'; } }, countryCode: (province) => { if (province.countryCode) { return province.countryCode; } else { const p = provinces.find((pr) => pr.code === province); return p?.countryCode || 'INVALID_PROVINCE'; } }, code: (province) => { if (province?.code) { return province?.code; } else { return province || 'INVALID_PROVINCE'; } } } }; ================================================ FILE: packages/evershop/src/modules/base/graphql/types/Route/Route.admin.graphql ================================================ """ Represents a route in the system """ type Route { id: String! isApi: Boolean! isAdmin: Boolean! name: String! path: String! method: [String!]! } extend type Query { routes: [Route!]! } ================================================ FILE: packages/evershop/src/modules/base/graphql/types/Route/Route.admin.resolvers.js ================================================ import { getRoutes } from '../../../../../lib/router/Router.js'; export default { Query: { routes: () => { const routes = getRoutes(); return routes.filter((route) => route.name); } } }; ================================================ FILE: packages/evershop/src/modules/base/graphql/types/Timezone/Timezone.graphql ================================================ """ A timezone """ type Timezone { name: String! code: String! } extend type Query { timezones: [Timezone]! } ================================================ FILE: packages/evershop/src/modules/base/graphql/types/Timezone/Timezone.resolvers.js ================================================ import { timezones } from '../../../../../lib/locale/timezones.js'; export default { Query: { timezones: () => timezones } }; ================================================ FILE: packages/evershop/src/modules/base/graphql/types/Url/Url.graphql ================================================ """ A query parameter for a URL """ input UrlParam { key: String! value: String! } extend type Query { url(routeId: String!, params: [UrlParam]): String! } ================================================ FILE: packages/evershop/src/modules/base/graphql/types/Url/Url.resolvers.js ================================================ import { buildUrl } from '../../../../../lib/router/buildUrl.js'; export default { Query: { url: (root, { routeId, params = [] }, { homeUrl }) => { const queries = []; params.forEach((param) => { // Check if the key is a string number if (param.key.match(/^[0-9]+$/)) { queries.push(param.value); } else { queries[param.key] = param.value; } }); return `${homeUrl}${buildUrl(routeId, queries)}`; } } }; ================================================ FILE: packages/evershop/src/modules/base/graphql/types/Version/Version.graphql ================================================ extend type Query { version: String! } ================================================ FILE: packages/evershop/src/modules/base/graphql/types/Version/Version.resolvers.js ================================================ import json from '@evershop/evershop/package.json' with { type: 'json' }; import { error } from '../../../../../lib/log/logger.js'; export default { Query: { version: () => { try { return json.version; } catch (e) { error(e); return 'unknown'; } } } }; ================================================ FILE: packages/evershop/src/modules/base/migration/Version-1.0.1.js ================================================ import { execute } from '@evershop/postgres-query-builder'; export default async (connection) => { await execute( connection, `CREATE TABLE "event" ( "event_id" INT GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1) PRIMARY KEY, "uuid" UUID NOT NULL DEFAULT gen_random_uuid (), "name" varchar NOT NULL, "data" json, "created_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "EVENT_UUID" UNIQUE ("uuid") )` ); }; ================================================ FILE: packages/evershop/src/modules/base/pages/admin/all/FormCss.tsx ================================================ import React from 'react'; export default function FormCss() { return null; } export const layout = { areaId: 'head', sortOrder: 5 }; ================================================ FILE: packages/evershop/src/modules/base/pages/admin/all/GlobalCss.tsx ================================================ import React from 'react'; import './global.scss'; export default function GlobalCss() { return null; } export const layout = { areaId: 'head', sortOrder: 5 }; ================================================ FILE: packages/evershop/src/modules/base/pages/admin/all/Layout.tsx ================================================ import Area from '@components/common/Area.js'; import React from 'react'; export default function AdminLayout() { return ( <>
    ); } export const layout = { areaId: 'body', sortOrder: 10 }; ================================================ FILE: packages/evershop/src/modules/base/pages/admin/all/Meta.tsx ================================================ import { Meta } from '@components/common/Meta.js'; import { Title } from '@components/common/Title.js'; import React from 'react'; interface SeoMetaProps { pageInfo: { title: string; description: string; }; } export default function SeoMeta({ pageInfo: { title, description } }: SeoMetaProps) { return ( <> <Meta name="description" content={description} /> </> ); } export const layout = { areaId: 'head', sortOrder: 5 }; export const query = ` query query { pageInfo { title description } } `; ================================================ FILE: packages/evershop/src/modules/base/pages/admin/all/TailwindCss.tsx ================================================ import React from 'react'; import './tailwind.css'; export default function TailwindCss() { return null; } export const layout = { areaId: 'head', sortOrder: 1 }; ================================================ FILE: packages/evershop/src/modules/base/pages/admin/all/[context]isAdmin[auth].js ================================================ export default (request, response) => { request.isAdmin = true; response.context.isAdmin = true; }; ================================================ FILE: packages/evershop/src/modules/base/pages/admin/all/form.scss ================================================ @reference './tailwind.css'; body.admin { .required-indicator { @apply text-destructive; } .form-field { @apply mb-4; label { @apply flex items-center mb-2 font-medium text-gray-700; .field-unit { @apply ml-2 text-sm text-gray-500; } } fieldset { legend { @apply flex items-center mb-2 font-medium text-gray-700; } } input[type='text'], input[type='email'], input[type='password'], input[type='number'], input[type='search'], input[type='tel'], input[type='url'], input[type='date'], input[type='time'], input[type='datetime-local'], input[type='color'], input[type='range'], input[type='file'], textarea, select { @apply block w-full px-3 py-2 text-base text-gray-900 placeholder-gray-300 border border-gray-300 rounded-md focus:outline-none focus:border-gray-300 sm:text-sm transition-colors; &:focus { box-shadow: 0 0 0 5px rgba(59, 130, 246, 0.1), 0 0 0 2px rgb(59, 130, 246); } &.error { @apply border-red-500; &:focus { box-shadow: 0 0 0 5px rgba(239, 68, 68, 0.1), 0 0 0 2px rgb(239, 68, 68); } } } select { @apply appearance-none bg-no-repeat bg-right pr-10; background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e"); background-position: right 0.75rem center; background-size: 1.25em 1.25em; &:focus { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%233b82f6' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e"); } & ~ .field-suffix { margin-left: -1.85rem; } } &.error { select { @apply border-red-500; background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23ef4444' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e"); &:focus { box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1), 0 0 0 1px rgb(239, 68, 68); background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23ef4444' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e"); } } input[type='checkbox'], input[type='radio'] { @apply border-red-500; } } input[type='range'] { @apply py-1; } input[type='file'] { @apply file:mr-3 file:py-2 file:px-3 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-gray-50 file:text-gray-700 hover:file:bg-gray-100; } input[type='color'] { @apply h-10 w-16 cursor-pointer; } input[type='checkbox'], input[type='radio'] { @apply w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded; &:focus { @apply outline-none border-blue-500; } } input[type='radio'] { @apply rounded-full; } .checkbox-group, .radio-group { @apply space-y-2; &.horizontal { @apply flex flex-wrap items-center gap-4 space-y-0; } .checkbox-item, .radio-item { @apply flex items-center; input { @apply mr-2; } label { @apply mb-0 font-normal cursor-pointer text-sm text-gray-700; &.disabled { @apply text-gray-400 cursor-not-allowed; } } } } .field-error { @apply text-red-600 text-sm mt-1; } input[readonly], textarea[readonly], select[readonly] { @apply bg-gray-50 text-gray-600 cursor-not-allowed border-gray-200; &:focus { @apply ring-0 border-gray-200 outline-none; } } &.readonly-field { input, textarea, select { @apply bg-gray-50 text-gray-600 cursor-not-allowed border-gray-200; &:focus { @apply ring-0 border-gray-200 outline-none; } } label { @apply text-gray-500; } .field-helper { @apply text-gray-400; } } input[disabled], textarea[disabled], select[disabled] { @apply bg-gray-100 text-gray-400 cursor-not-allowed opacity-60; } .field-helper { @apply text-gray-500 text-sm mt-1; } .field-tooltip { @apply relative inline-block ml-1; .tooltip-trigger { @apply w-4 h-4 text-gray-400 cursor-help; } .tooltip-content { @apply absolute z-50 px-3 py-2 text-sm text-white bg-gray-900 rounded-md shadow-lg opacity-0 invisible transition-all duration-200; @apply bottom-full left-1/2 transform -translate-x-1/2 mb-2; @apply min-w-max max-w-xs; &::after { content: ''; @apply absolute top-full left-1/2 transform -translate-x-1/2; border: 4px solid transparent; border-top-color: #111827; } &.show { @apply opacity-100 visible; } } } .color-field-container { @apply flex items-center gap-2; input[type='text'] { @apply flex-1; } input[type='color'] { @apply flex-shrink-0; } } .number-field-container { @apply relative; .number-unit { @apply absolute right-3 top-1/2 transform -translate-y-1/2 text-sm text-gray-500 pointer-events-none; } input { &.has-unit { @apply pr-12; } } } .file-field-info { @apply text-xs text-gray-500 mt-1; } .color-input-group { @apply flex items-center space-x-2; .color-picker { @apply w-16 h-10 p-1 border rounded cursor-pointer; } .color-input { @apply flex-1 px-3 py-2 border rounded-md; } } .range-value { @apply ml-2 text-sm text-gray-500; } .range-labels { @apply flex justify-between text-xs text-gray-500 mt-1; } .file-size-hint { @apply mt-1 text-sm text-gray-500; } .file-list { @apply mt-2; .file-list-label { @apply text-sm text-gray-600; } .file-items { @apply text-sm text-gray-500 list-disc list-inside; } } .react-select { &__control { @apply border border-gray-300 rounded-md transition-colors; &--is-focused { @apply border-blue-500; box-shadow: 0 0 0 1px rgb(59, 130, 246); } } &__menu { @apply bg-white border border-gray-300 rounded-md shadow-lg mt-1; } &__option { @apply px-3 py-2 cursor-pointer; &--is-focused { @apply bg-blue-50; } &--is-selected { @apply bg-blue-600 text-white; } } &__multi-value { @apply bg-blue-100 rounded; &__label { @apply text-blue-800; } &__remove { @apply text-blue-600 hover:bg-red-500 hover:text-white rounded-r; } } } &.error .react-select { &__control { @apply border-red-500; &--is-focused { @apply border-red-500; box-shadow: 0 0 0 2px rgb(239, 68, 68); } } } .react-select { &__input { input { &:focus { box-shadow: none !important; outline: none !important; } } } &__control { &:focus-within { @apply shadow-none; box-shadow: none !important; } } } } .react-select__input input:focus, .react-select__input input:focus-visible, .react-select__input input { box-shadow: none !important; outline: none !important; } div[class*='react-select'] input:focus { box-shadow: none !important; outline: none !important; } } ================================================ FILE: packages/evershop/src/modules/base/pages/admin/all/global.scss ================================================ body { font-size: 0.875rem; } .header { height: 3.5rem; width: 100%; padding: 0 1rem; box-sizing: border-box; display: flex; position: fixed; top: 0; left: 0; background-color: #fff; box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.15); z-index: 1000; } .content-wrapper { margin-top: 3.5rem; min-height: 100vh; .main-content { overflow: hidden; margin-left: 15rem; .main-content-inner { padding: 1.25rem 1.875rem; min-height: 100vh; } } } .footer { padding-left: 1.875rem; margin-top: 1.25rem; padding-bottom: 0.625rem; border-top: 1px solid var(--divider); padding-top: 0.625rem; } ================================================ FILE: packages/evershop/src/modules/base/pages/admin/all/shadcn.css ================================================ @theme inline { @keyframes accordion-down { from { height: 0; } to { height: var( --radix-accordion-content-height, var(--accordion-panel-height, auto) ); } } @keyframes accordion-up { from { height: var( --radix-accordion-content-height, var(--accordion-panel-height, auto) ); } to { height: 0; } } } /* Custom variants */ @custom-variant data-open { &:where([data-state='open']), &:where([data-open]:not([data-open='false'])) { @slot; } } @custom-variant data-closed { &:where([data-state='closed']), &:where([data-closed]:not([data-closed='false'])) { @slot; } } @custom-variant data-checked { &:where([data-state='checked']), &:where([data-checked]:not([data-checked='false'])) { @slot; } } @custom-variant data-unchecked { &:where([data-state='unchecked']), &:where([data-unchecked]:not([data-unchecked='false'])) { @slot; } } @custom-variant data-selected { &:where([data-selected='true']) { @slot; } } @custom-variant data-disabled { &:where([data-disabled='true']), &:where([data-disabled]:not([data-disabled='false'])) { @slot; } } @custom-variant data-active { &:where([data-state='active']), &:where([data-active]:not([data-active='false'])) { @slot; } } @custom-variant data-horizontal { &:where([data-orientation='horizontal']) { @slot; } } @custom-variant data-vertical { &:where([data-orientation='vertical']) { @slot; } } @utility no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; &::-webkit-scrollbar { display: none; } } ================================================ FILE: packages/evershop/src/modules/base/pages/admin/all/tailwind.css ================================================ @import 'tailwindcss'; @import 'tw-animate-css'; @import './shadcn.css'; @custom-variant dark (&:is(.dark *)); @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: var(--font-sans); --font-mono: var(--font-geist-mono); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-accent: var(--sidebar-accent); --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); --color-sidebar-primary: var(--sidebar-primary); --color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar: var(--sidebar); --color-chart-5: var(--chart-5); --color-chart-4: var(--chart-4); --color-chart-3: var(--chart-3); --color-chart-2: var(--chart-2); --color-chart-1: var(--chart-1); --color-ring: var(--ring); --color-input: var(--input); --color-border: var(--border); --color-divider: var(--divider); --color-destructive: var(--destructive); --color-accent-foreground: var(--accent-foreground); --color-accent: var(--accent); --color-muted-foreground: var(--muted-foreground); --color-muted: var(--muted); --color-secondary-foreground: var(--secondary-foreground); --color-secondary: var(--secondary); --color-primary-foreground: var(--primary-foreground); --color-primary: var(--primary); --color-popover-foreground: var(--popover-foreground); --color-popover: var(--popover); --color-card-foreground: var(--card-foreground); --color-card: var(--card); --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); --radius-2xl: calc(var(--radius) + 8px); --radius-3xl: calc(var(--radius) + 12px); --radius-4xl: calc(var(--radius) + 16px); } :root { --radius: 0.325rem; --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); --card: oklch(1 0 0); --card-foreground: oklch(0.145 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.145 0 0); --primary: oklch(0.53 0.11 167.71); --primary-foreground: oklch(0.985 0 0); --secondary: oklch(0.97 0 0); --secondary-foreground: oklch(0.205 0 0); --muted: oklch(0.97 0 0); --muted-foreground: oklch(0.556 0 0); --accent: oklch(0.97 0 0); --accent-foreground: oklch(0.205 0 0); --destructive: oklch(0.577 0.245 27.325); --border: oklch(0.922 0 0); --divider: oklch(0.922 0 0); --input: oklch(0.922 0 0); --ring: oklch(70.9% 0.00008 271.152); --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); --chart-3: oklch(0.398 0.07 227.392); --chart-4: oklch(0.828 0.189 84.429); --chart-5: oklch(0.769 0.188 70.08); --sidebar: oklch(0.985 0 0); --sidebar-foreground: oklch(0.145 0 0); --sidebar-primary: oklch(0.205 0 0); --sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-accent: oklch(0.97 0 0); --sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-border: oklch(0.922 0 0); --sidebar-ring: oklch(0.708 0 0); } .dark { --background: oklch(0.15 0.01 180); --foreground: oklch(0.95 0.008 180); --card: oklch(0.18 0.012 180); --card-foreground: oklch(0.95 0.008 180); --popover: oklch(0.17 0.012 180); --popover-foreground: oklch(0.95 0.008 180); /* Primary: Brighter teal for dark mode */ --primary: oklch(0.62 0.14 172); --primary-foreground: oklch(0.12 0.02 172); /* Secondary: Darker teal variant */ --secondary: oklch(0.25 0.04 172); --secondary-foreground: oklch(0.9 0.04 172); /* Muted: Dark with teal tint */ --muted: oklch(0.22 0.015 172); --muted-foreground: oklch(0.68 0.02 180); /* Accent: Vibrant teal for dark backgrounds */ --accent: oklch(0.28 0.05 172); --accent-foreground: oklch(0.9 0.04 172); /* Destructive: Softer red for dark mode */ --destructive: oklch(0.62 0.24 27); --border: oklch(0.26 0.02 172); --input: oklch(0.24 0.02 172); --ring: oklch(0.62 0.14 172); /* Chart colors: Adjusted for dark mode visibility */ --chart-1: oklch(0.7 0.16 172); --chart-2: oklch(0.65 0.18 200); --chart-3: oklch(0.75 0.18 90); --chart-4: oklch(0.7 0.15 320); --chart-5: oklch(0.7 0.16 30); /* Sidebar: Dark teal tinted background */ --sidebar: oklch(0.16 0.015 172); --sidebar-foreground: oklch(0.92 0.01 172); --sidebar-primary: oklch(0.62 0.14 172); --sidebar-primary-foreground: oklch(0.12 0.02 172); --sidebar-accent: oklch(0.24 0.03 172); --sidebar-accent-foreground: oklch(0.9 0.04 172); --sidebar-border: oklch(0.28 0.025 172); --sidebar-ring: oklch(0.62 0.14 172); } ================================================ FILE: packages/evershop/src/modules/base/pages/frontStore/all/Base.tsx ================================================ import Area from '@components/common/Area.js'; import { LoadingBar } from '@components/common/LoadingBar.js'; import { CartProvider, CartData } from '@components/frontStore/cart/CartContext.js'; import { CustomerProvider, Customer } from '@components/frontStore/customer/CustomerContext.js'; import { Footer } from '@components/frontStore/Footer.js'; import { Header } from '@components/frontStore/Header.js'; import React from 'react'; interface BaseProps { myCart: CartData; customer: Customer; themeConfig: { copyRight: string; }; addMineCartItemApi: string; loginApi: string; logoutApi: string; registerApi: string; } export default function Base({ myCart, customer, themeConfig, addMineCartItemApi, loginApi, logoutApi, registerApi }: BaseProps) { return ( <CustomerProvider initialCustomer={customer} loginAPI={loginApi} logoutAPI={logoutApi} registerAPI={registerApi} > <CartProvider cart={myCart} query={`${query}\n${fragments}`} addMineCartItemApi={addMineCartItemApi} > <LoadingBar /> <Header /> <main className="content"> <Area id="content" noOuter /> </main> <Footer copyRight={themeConfig.copyRight} /> </CartProvider> </CustomerProvider> ); } export const layout = { areaId: 'body', sortOrder: 1 }; export const query = ` query Query { myCart { ...ShoppingCart addItemApi addPaymentMethodApi addShippingMethodApi addContactInfoApi addAddressApi addNoteApi checkoutApi applyCouponApi removeCouponApi availableShippingMethods { code name cost { value text } } availablePaymentMethods { code name } items { ...ShoppingCartItem cartItemId removeApi updateQtyApi errors } } customer: currentCustomer { customerId uuid email fullName groupId createdAt { value text } addAddressApi addresses { addressId uuid fullName telephone address1 address2 city province { code name } country { code name } postcode isDefault updateApi deleteApi } orders { ...ShoppingCart orderId status { name code badge } orderNumber shipmentStatus { name code badge } paymentStatus { name code badge } items { ...ShoppingCartItem orderItemId } } } themeConfig { copyRight } addMineCartItemApi: url(routeId: "addMineCartItem") loginApi: url(routeId: "customerLoginJson") registerApi: url(routeId: "createCustomer") logoutApi: url(routeId: "customerLogoutJson") } `; export const fragments = ` fragment ShoppingCart on ShoppingCart { uuid currency customerId customerGroupId customerEmail customerFullName coupon noShippingRequired shippingMethod shippingMethodName paymentMethod paymentMethodName shippingNote taxAmount { value text } totalTaxAmount { value text } discountAmount { value text } shippingFeeExclTax { value text } shippingFeeInclTax { value text } shippingTaxAmount { value text } subTotal { value text } subTotalInclTax { value text } subTotalWithDiscount { value text } subTotalWithDiscountInclTax { value text } totalQty totalWeight { value unit } taxAmountBeforeDiscount { value text } grandTotal { value text } billingAddress { fullName telephone address1 address2 city province { name code } country { name code } postcode } shippingAddress { fullName telephone address1 address2 city province { name code } country { name code } postcode } createdAt { value text } updatedAt { value text } } fragment ShoppingCartItem on ShoppingCartItem { uuid productId productSku productName thumbnail productWeight { value unit } productPrice { value text } productPriceInclTax { value text } qty finalPrice { value text } finalPriceInclTax { value text } taxPercent taxAmount { value text } taxAmountBeforeDiscount { value text } discountAmount { value text } lineTotal { value text } subTotal { value text } lineTotalWithDiscount { value text } lineTotalWithDiscountInclTax { value text } lineTotalInclTax { value text } total { value text } variantGroupId variantOptions { attributeCode attributeName attributeId optionId optionText } productUrl } `; ================================================ FILE: packages/evershop/src/modules/base/pages/frontStore/all/Breadcrumb.tsx ================================================ import { Breadcrumb as BreadcrumbRoot, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from '@components/common/ui/Breadcrumb.js'; import React from 'react'; interface BreadcrumbProps { pageInfo: { breadcrumbs: Array<{ title: string; url: string; }>; }; } function Breadcrumb({ pageInfo: { breadcrumbs } }: BreadcrumbProps) { return breadcrumbs.length ? ( <div className="page-width"> <div className="py-5"> <BreadcrumbRoot> <BreadcrumbList> {breadcrumbs.map((breadcrumb, index) => ( <React.Fragment key={index}> <BreadcrumbItem> {index === breadcrumbs.length - 1 ? ( <BreadcrumbPage>{breadcrumb.title}</BreadcrumbPage> ) : ( <BreadcrumbLink href={breadcrumb.url}> {breadcrumb.title} </BreadcrumbLink> )} </BreadcrumbItem> {index < breadcrumbs.length - 1 && <BreadcrumbSeparator />} </React.Fragment> ))} </BreadcrumbList> </BreadcrumbRoot> </div> </div> ) : null; } export const query = ` query query { pageInfo { breadcrumbs { title url } } } `; export const layout = { areaId: 'content', sortOrder: 0 }; export default Breadcrumb; ================================================ FILE: packages/evershop/src/modules/base/pages/frontStore/all/GlobalCss.tsx ================================================ import React from 'react'; import './global.scss'; export default function GlobalCss() { return null; } export const layout = { areaId: 'head', sortOrder: 5 }; ================================================ FILE: packages/evershop/src/modules/base/pages/frontStore/all/HeadTags.tsx ================================================ import { Og } from '@components/frontStore/Og.js'; import React, { LinkHTMLAttributes, MetaHTMLAttributes, ScriptHTMLAttributes } from 'react'; interface HeadTagsProps { pageInfo: { title: string; description: string; keywords: string[]; canonicalUrl: string; favicon: string; ogInfo: { locale: string; title: string; description: string; image: string; url: string; type: 'website' | 'article' | 'product' | string; siteName: string; twitterCard: 'summary' | 'summary_large_image' | 'app' | 'player'; twitterSite: string; twitterCreator: string; twitterImage: string; }; }; themeConfig: { headTags: { metas: Array<MetaHTMLAttributes<HTMLMetaElement>>; links: Array<LinkHTMLAttributes<HTMLLinkElement>>; scripts: Array<ScriptHTMLAttributes<HTMLScriptElement>>; base?: { href: string; target: '_blank' | '_self' | '_parent' | '_top'; }; }; }; } export default function HeadTags({ pageInfo: { title, description, keywords, canonicalUrl, ogInfo, favicon }, themeConfig: { headTags: { metas, links, scripts, base } } }: HeadTagsProps) { React.useEffect(() => { const head = document.querySelector('head'); scripts.forEach((script) => { const scriptElement = document.createElement('script'); Object.keys(script).forEach((key) => { if (script[key]) { scriptElement[key] = script[key]; } }); head?.appendChild(scriptElement); }); }, []); return ( <> <title>{title} {metas.map((meta, index) => ( ))} {links.map((link, index) => ( ))} {scripts.map((script, index) => (