├── src ├── web │ └── assets │ │ └── dist │ │ ├── assets │ │ ├── app--MQxI0fB.js │ │ ├── app-BgTVgbp1.css │ │ ├── app--MQxI0fB.js.map │ │ ├── welcome-BP_XNmLM.js.gz │ │ ├── welcome-BP_XNmLM.js.map.gz │ │ └── welcome-BP_XNmLM.js │ │ ├── manifest.json │ │ └── img │ │ └── InstantAnalytics-icon.svg ├── templates │ ├── _includes │ │ ├── macros.twig │ │ └── gtag.twig │ ├── _layouts │ │ └── instantanalytics-cp.twig │ ├── welcome.twig │ └── settings.twig ├── assetbundles │ └── instantanalytics │ │ ├── InstantAnalyticsAsset.php │ │ └── InstantAnalyticsWelcomeAsset.php ├── ga4 │ ├── Service.php │ ├── events │ │ └── PageViewEvent.php │ ├── ComponentFactory.php │ └── Analytics.php ├── controllers │ └── TrackController.php ├── icon.svg ├── translations │ └── en │ │ └── instant-analytics.php ├── services │ ├── ServicesTrait.php │ ├── Ga4.php │ └── Commerce.php ├── variables │ └── InstantAnalyticsVariable.php ├── config.php ├── twigextensions │ └── InstantAnalyticsTwigExtension.php ├── models │ └── Settings.php ├── helpers │ ├── Field.php │ └── Analytics.php └── InstantAnalytics.php ├── phpstan.neon ├── CHANGELOG.md ├── ecs.php ├── Makefile ├── composer.json ├── LICENSE.md └── README.md /src/web/assets/dist/assets/app--MQxI0fB.js: -------------------------------------------------------------------------------- 1 | 2 | //# sourceMappingURL=app--MQxI0fB.js.map 3 | -------------------------------------------------------------------------------- /src/web/assets/dist/assets/app-BgTVgbp1.css: -------------------------------------------------------------------------------- 1 | .block{display:block}.inline-block{display:inline-block} 2 | -------------------------------------------------------------------------------- /src/web/assets/dist/assets/app--MQxI0fB.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"app--MQxI0fB.js","sources":[],"sourcesContent":[],"names":[],"mappings":""} -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - %currentWorkingDirectory%/vendor/craftcms/phpstan/phpstan.neon 3 | 4 | parameters: 5 | level: 5 6 | paths: 7 | - src 8 | -------------------------------------------------------------------------------- /src/web/assets/dist/assets/welcome-BP_XNmLM.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nystudio107/craft-instantanalytics-ga4/develop-v5/src/web/assets/dist/assets/welcome-BP_XNmLM.js.gz -------------------------------------------------------------------------------- /src/web/assets/dist/assets/welcome-BP_XNmLM.js.map.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nystudio107/craft-instantanalytics-ga4/develop-v5/src/web/assets/dist/assets/welcome-BP_XNmLM.js.map.gz -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Instant Analytics GA4 Changelog 2 | 3 | ## 5.0.1 - 2024.09.14 4 | ### Fixed 5 | * Fixed an inadvertant dependency on SEOmatic ([#35](https://github.com/nystudio107/craft-instantanalytics-ga4/issues/35)) 6 | 7 | ## 5.0.0 - 2024.07.09 8 | ### Added 9 | * Initial Craft 5 release 10 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | paths([ 8 | __DIR__ . '/src', 9 | __FILE__, 10 | ]); 11 | $ecsConfig->parallel(); 12 | $ecsConfig->sets([SetList::CRAFT_CMS_4]); 13 | }; 14 | -------------------------------------------------------------------------------- /src/templates/_includes/macros.twig: -------------------------------------------------------------------------------- 1 | {% macro configWarning(setting, file) -%} 2 | {%- set configArray = craft.app.config.getConfigFromFile(file) -%} 3 | {%- if configArray[setting] is defined -%} 4 | {{- "This is being overridden by the `#{setting}` setting in the `config/#{file}.php` file." |raw }} 5 | {%- else -%} 6 | {{ false }} 7 | {%- endif -%} 8 | {%- endmacro %} 9 | -------------------------------------------------------------------------------- /src/templates/_includes/gtag.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /src/templates/_layouts/instantanalytics-cp.twig: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/cp" %} 2 | 3 | {% block head %} 4 | {{ parent() }} 5 | {% set tagOptions = { 6 | 'depends': [ 7 | 'nystudio107\\instantanalyticsGa4\\assetbundles\\instantanalytics\\InstantAnalyticsAsset' 8 | ], 9 | } %} 10 | {{ craft.instantAnalytics.register('src/js/app.ts', false, tagOptions, tagOptions) }} 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /src/web/assets/dist/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "src/js/app.ts": { 3 | "file": "assets/app--MQxI0fB.js", 4 | "name": "app", 5 | "src": "src/js/app.ts", 6 | "isEntry": true, 7 | "css": [ 8 | "assets/app-BgTVgbp1.css" 9 | ] 10 | }, 11 | "src/js/welcome.ts": { 12 | "file": "assets/welcome-BP_XNmLM.js", 13 | "name": "welcome", 14 | "src": "src/js/welcome.ts", 15 | "isEntry": true 16 | } 17 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAJOR_VERSION?=5 2 | PLUGINDEV_PROJECT_DIR?=/Users/andrew/webdev/sites/plugindev/cms_v${MAJOR_VERSION}/ 3 | VENDOR?=nystudio107 4 | PROJECT_PATH?=${VENDOR}/$(shell basename $(CURDIR)) 5 | 6 | .PHONY: dev docs release 7 | 8 | # Start up the buildchain dev server 9 | dev: 10 | ${MAKE} -C buildchain/ dev 11 | # Start up the docs dev server 12 | docs: 13 | ${MAKE} -C docs/ dev 14 | # Run code quality tools, tests, and build the buildchain & docs in preparation for a release 15 | release: --code-quality --code-tests --buildchain-clean-build --docs-clean-build 16 | # The internal targets used by the dev & release targets 17 | --buildchain-clean-build: 18 | ${MAKE} -C buildchain/ clean 19 | ${MAKE} -C buildchain/ image-build 20 | ${MAKE} -C buildchain/ build 21 | --code-quality: 22 | ${MAKE} -C ${PLUGINDEV_PROJECT_DIR} -- ecs check vendor/${PROJECT_PATH}/src --fix 23 | ${MAKE} -C ${PLUGINDEV_PROJECT_DIR} -- phpstan analyze -c vendor/${PROJECT_PATH}/phpstan.neon 24 | --code-tests: 25 | --docs-clean-build: 26 | ${MAKE} -C docs/ clean 27 | ${MAKE} -C docs/ image-build 28 | ${MAKE} -C docs/ fix 29 | -------------------------------------------------------------------------------- /src/assetbundles/instantanalytics/InstantAnalyticsAsset.php: -------------------------------------------------------------------------------- 1 | sourcePath = '@nystudio107/instantanalyticsGa4/web/assets/dist'; 29 | 30 | // define the dependencies 31 | $this->depends = [ 32 | CpAsset::class, 33 | VueAsset::class, 34 | ]; 35 | 36 | parent::init(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/assetbundles/instantanalytics/InstantAnalyticsWelcomeAsset.php: -------------------------------------------------------------------------------- 1 | sourcePath = '@nystudio107/instantanalyticsGa4/web/assets/dist'; 33 | 34 | $this->depends = [ 35 | CpAsset::class, 36 | VueAsset::class, 37 | InstantAnalyticsAsset::class, 38 | ]; 39 | 40 | parent::init(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ga4/Service.php: -------------------------------------------------------------------------------- 1 | additionalParams[$name]); 29 | } else { 30 | $this->additionalParams[$name] = $value; 31 | } 32 | } 33 | 34 | public function deleteAdditionalQueryParam(string $name): void 35 | { 36 | unset($this->additionalParams[$name]); 37 | } 38 | 39 | public function getQueryParameters(): array 40 | { 41 | $parameters = parent::getQueryParameters(); 42 | 43 | // Return without overwriting existing 44 | return $parameters + $this->additionalParams; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/ga4/events/PageViewEvent.php: -------------------------------------------------------------------------------- 1 | eventName, $paramList); 34 | } 35 | 36 | public function validate() 37 | { 38 | parent::validate(); 39 | 40 | if (empty($this->getPageTitle())) { 41 | throw new ValidationException('Field "page_title" is required.', ErrorCode::VALIDATION_FIELD_REQUIRED, 'page_title'); 42 | } 43 | 44 | if (empty($this->getPageLocation())) { 45 | throw new ValidationException('Field "page_location" is required if "value" is set', ErrorCode::VALIDATION_FIELD_REQUIRED, 'page_location'); 46 | } 47 | 48 | return true; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/controllers/TrackController.php: -------------------------------------------------------------------------------- 1 | ga4->addPageViewEvent($url, $title); 43 | $this->redirect($url, 200); 44 | } 45 | 46 | /** 47 | * @param string $url 48 | * @param string $eventName 49 | * @param array $params 50 | */ 51 | public function actionTrackEventUrl( 52 | string $url, 53 | string $eventName = '', 54 | array $params = [], 55 | ): void { 56 | InstantAnalytics::$plugin->ga4->addSimpleEvent($url, $eventName, $params); 57 | 58 | $this->redirect($url, 200); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 12 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/web/assets/dist/img/InstantAnalytics-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 12 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nystudio107/craft-instantanalytics-ga4", 3 | "description": "Instant Analytics brings full Google GA4 server-side analytics support to your Twig templates and automatic Craft Commerce integration", 4 | "type": "craft-plugin", 5 | "version": "5.0.1", 6 | "keywords": [ 7 | "craft", 8 | "cms", 9 | "craftcms", 10 | "craft-plugin", 11 | "instant analytics", 12 | "google", 13 | "analytics", 14 | "server-side", 15 | "ga4" 16 | ], 17 | "support": { 18 | "docs": "https://nystudio107.com/docs/instant-analytics-ga4/", 19 | "issues": "https://nystudio107.com/plugins/instant-analytics-ga4/support", 20 | "source": "https://github.com/nystudio107/craft-instantanalytics-ga4" 21 | }, 22 | "license": "proprietary", 23 | "authors": [ 24 | { 25 | "name": "nystudio107", 26 | "homepage": "https://nystudio107.com" 27 | } 28 | ], 29 | "require": { 30 | "br33f/php-ga4-mp": "^0.1.0", 31 | "craftcms/cms": "^5.0.0", 32 | "nystudio107/craft-plugin-vite": "^5.0.0", 33 | "jaybizzle/crawler-detect": "^1.2.37" 34 | }, 35 | "require-dev": { 36 | "craftcms/ecs": "dev-main", 37 | "craftcms/phpstan": "dev-main", 38 | "craftcms/rector": "dev-main", 39 | "craftcms/ckeditor": "^4.0.0", 40 | "craftcms/commerce": "^5.0.0", 41 | "craftcms/redactor": "^4.0.0", 42 | "nystudio107/craft-seomatic": "^5.0.0" 43 | }, 44 | "scripts": { 45 | "phpstan": "phpstan --ansi --memory-limit=1G", 46 | "check-cs": "ecs check --ansi", 47 | "fix-cs": "ecs check --fix --ansi" 48 | }, 49 | "config": { 50 | "allow-plugins": { 51 | "craftcms/plugin-installer": true, 52 | "yiisoft/yii2-composer": true 53 | }, 54 | "optimize-autoloader": true, 55 | "sort-packages": true 56 | }, 57 | "autoload": { 58 | "psr-4": { 59 | "nystudio107\\instantanalyticsGa4\\": "src/" 60 | } 61 | }, 62 | "extra": { 63 | "class": "nystudio107\\instantanalyticsGa4\\InstantAnalytics", 64 | "handle": "instant-analytics-ga4", 65 | "name": "Instant Analytics GA4" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © nystudio107 2 | 3 | Permission is hereby granted to any person obtaining a copy of this software 4 | (the “Software”) to use, copy, modify, merge, publish and/or distribute copies 5 | of the Software, and to permit persons to whom the Software is furnished to do 6 | so, subject to the following conditions: 7 | 8 | 1. **Don’t plagiarize.** The above copyright notice and this license shall be 9 | included in all copies or substantial portions of the Software. 10 | 11 | 2. **Don’t use the same license on more than one project.** Each licensed copy 12 | of the Software shall be actively installed in no more than one production 13 | environment at a time. 14 | 15 | 3. **Don’t mess with the licensing features.** Software features related to 16 | licensing shall not be altered or circumvented in any way, including (but 17 | not limited to) license validation, payment prompts, feature restrictions, 18 | and update eligibility. 19 | 20 | 4. **Pay up.** Payment shall be made immediately upon receipt of any notice, 21 | prompt, reminder, or other message indicating that a payment is owed. 22 | 23 | 5. **Follow the law.** All use of the Software shall not violate any applicable 24 | law or regulation, nor infringe the rights of any other person or entity. 25 | 26 | Failure to comply with the foregoing conditions will automatically and 27 | immediately result in termination of the permission granted hereby. This 28 | license does not include any right to receive updates to the Software or 29 | technical support. Licensees bear all risk related to the quality and 30 | performance of the Software and any modifications made or obtained to it, 31 | including liability for actual and consequential harm, such as loss or 32 | corruption of data, and any necessary service, repair, or correction. 33 | 34 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 35 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 36 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 37 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 38 | LIABILITY, INCLUDING SPECIAL, INCIDENTAL AND CONSEQUENTIAL DAMAGES, WHETHER IN 39 | AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 40 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/nystudio107/craft-instantanalytics-ga4/badges/quality-score.png?b=v5)](https://scrutinizer-ci.com/g/nystudio107/craft-instantanalytics-ga4/?branch=v5) [![Code Coverage](https://scrutinizer-ci.com/g/nystudio107/craft-instantanalytics-ga4/badges/coverage.png?b=v5)](https://scrutinizer-ci.com/g/nystudio107/craft-instantanalytics-ga4/?branch=v5) [![Build Status](https://scrutinizer-ci.com/g/nystudio107/craft-instantanalytics-ga4/badges/build.png?b=v5)](https://scrutinizer-ci.com/g/nystudio107/craft-instantanalytics-ga4/build-status/v5) [![Code Intelligence Status](https://scrutinizer-ci.com/g/nystudio107/craft-instantanalytics-ga4/badges/code-intelligence.svg?b=v5)](https://scrutinizer-ci.com/code-intelligence) 2 | 3 | # Instant Analytics GA4 plugin for Craft CMS 5.x 4 | 5 | Instant Analytics GA4 brings full Google Analytics support to your Twig templates and automatic Craft Commerce integration with Google Enhanced Ecommerce. 6 | 7 | **Note**: _This is an entirely rewritten plugin to reflect the new data model & API in GA4._ 8 | 9 | ![Screenshot](./docs/docs/resources/img/plugin-banner.jpg) 10 | 11 | **Note**: _The license fee for this plugin is $59.00 via the Craft Plugin Store._ 12 | 13 | ## Requirements 14 | 15 | This plugin requires Craft CMS 5.0.0 or later. Commerce 5 or later is required for Google Analytics Enhanced eCommerce support. 16 | 17 | ## Installation 18 | 19 | To install the plugin, follow these instructions. 20 | 21 | 1. Open your terminal and go to your Craft project: 22 | 23 | cd /path/to/project 24 | 25 | 2. Then tell Composer to load the plugin: 26 | 27 | composer require nystudio107/craft-instantanalytics-ga4 28 | 29 | 3. Install the plugin via `./craft install/plugin instant-analytics-ga4` via the CLI, or in the Control Panel, go to Settings → Plugins and click the “Install” button for Instant Analytics. 30 | 31 | You can also install Instant Analytics via the **Plugin Store** in the Craft Control Panel. 32 | 33 | ## Documentation 34 | 35 | Click here -> [Instant Analytics GA4 Documentation](https://nystudio107.com/plugins/instant-analytics-ga4/documentation) 36 | 37 | ## Instant Analytics GA4 Roadmap 38 | 39 | Some things to do, and ideas for potential features: 40 | 41 | * Stable release 42 | 43 | Brought to you by [nystudio107](http://nystudio107.com) 44 | -------------------------------------------------------------------------------- /src/templates/welcome.twig: -------------------------------------------------------------------------------- 1 | {# @var craft \craft\web\twig\variables\CraftVariable #} 2 | {% extends 'instant-analytics-ga4/_layouts/instantanalytics-cp.twig' %} 3 | 4 | {% set title = 'Welcome to Instant Analytics!' %} 5 | 6 | {% set docsUrl = "https://github.com/nystudio107/craft-instantanalytics-ga4/" %} 7 | {% set linkGetStarted = url('settings/plugins/instant-analytics-ga4') %} 8 | 9 | {% do view.registerAssetBundle("nystudio107\\instantanalyticsGa4\\assetbundles\\instantanalytics\\InstantAnalyticsWelcomeAsset") %} 10 | {% set baseAssetsUrl = view.getAssetManager().getPublishedUrl('@nystudio107/instantanalyticsGa4/web/assets/dist', true) %} 11 | 12 | {% set crumbs = [ 13 | { label: "Instant Analytics", url: url('instant-analytics') }, 14 | { label: "Welcome"|t, url: url('instant-analytics-ga4/welcome') }, 15 | ] %} 16 | 17 | {% block head %} 18 | {{ parent() }} 19 | {% set tagOptions = { 20 | 'depends': [ 21 | 'nystudio107\\instantanalyticsGa4\\assetbundles\\instantanalytics\\InstantAnalyticsAsset' 22 | ], 23 | } %} 24 | {{ craft.instantAnalytics.register('src/js/welcome.ts', false, tagOptions, tagOptions) }} 25 | {% endblock %} 26 | 27 | {% block content %} 28 |
29 |
30 |
31 | 33 |

Thanks for using Instant Analytics GA4!

34 |

Instant Analytics GA4 brings full Google Analytics 4 support to your Twig templates and automatic Craft 35 | Commerce 36 | integration with Google Enhanced Ecommerce.

37 |

Instant Analytics GA4 also lets you track otherwise untrackable assets & events with Google Analytics 4, and 38 | eliminates the need for Javascript tracking.

39 | 40 |

For more information, please see the documentation. 41 |

42 |

43 |   44 |

45 |

46 | 47 | 48 | 49 |

50 |
51 |
52 |

53 | Brought to you by nystudio107 54 |

55 |
56 | {% endblock %} 57 | -------------------------------------------------------------------------------- /src/translations/en/instant-analytics.php: -------------------------------------------------------------------------------- 1 | '{name} plugin loaded', 25 | 'Craft Commerce is not installed' => 'Craft Commerce is not installed', 26 | 'Created sendPageView for: {eventCategory} - {eventAction} - {eventLabel} - {eventValue}' => 'Created sendPageView for: {eventCategory} - {eventAction} - {eventLabel} - {eventValue}', 27 | 'Created eventTrackingUrl for: {trackingUrl}' => 'Created eventTrackingUrl for: {trackingUrl}', 28 | 'Created pageViewTrackingUrl for: {trackingUrl}' => 'Created pageViewTrackingUrl for: {trackingUrl}', 29 | 'Analytics excluded for:: {requestIp} due to: `{setting}`' => 'Analytics excluded for:: {requestIp} due to: `{setting}`', 30 | 'Created sendPageView for: {url} - {title}' => 'Created sendPageView for: {url} - {title}', 31 | 'Created generic analytics object' => 'Created generic analytics object', 32 | 'Analytics not sent because googleAnalyticsTracking is not set' => 'Analytics not sent because googleAnalyticsTracking is not set', 33 | 'pageView sent, response:: {response}' => 'pageView sent, response:: {response}', 34 | 'addCommerceCheckoutStep step: `{step}` with option: `{option}`' => 'addCommerceCheckoutStep step: `{step}` with option: `{option}`', 35 | 'removeFromCart for `Commerce` - `Remove to Cart` - `{title}` - `{quantity}`' => 'removeFromCart for `Commerce` - `Remove to Cart` - `{title}` - `{quantity}`', 36 | 'Manifest file not found at: {manifestPath}' => 'Manifest file not found at: {manifestPath}', 37 | 'orderComplete for `Commerce` - `Purchase` - `{reference}` - `{price}`' => 'orderComplete for `Commerce` - `Purchase` - `{reference}` - `{price}`', 38 | 'addCommerceProductImpression for `{sku}` - `{name}` - `{name}` - `{index}`' => 'addCommerceProductImpression for `{sku}` - `{name}` - `{name}` - `{index}`', 39 | 'Module does not exist in the manifest: {moduleName}' => 'Module does not exist in the manifest: {moduleName}', 40 | 'addCommerceProductDetailView for `{sku}` - `{name} - `{name}`' => 'addCommerceProductDetailView for `{sku}` - `{name} - `{name}`', 41 | 'addToCart for `Commerce` - `Add to Cart` - `{title}` - `{quantity}`' => 'addToCart for `Commerce` - `Add to Cart` - `{title}` - `{quantity}`', 42 | 'orderComplete for `Commerce` - `Purchase` - `{number}` - `{price}`' => 'orderComplete for `Commerce` - `Purchase` - `{number}` - `{price}`', 43 | 'addCommerceProductDetailView for `{sku}` - `{name}`' => 'addCommerceProductDetailView for `{sku}` - `{name}`', 44 | ]; 45 | -------------------------------------------------------------------------------- /src/services/ServicesTrait.php: -------------------------------------------------------------------------------- 1 | = 8.2, and config() is called before __construct(), 38 | // so we can't extract it from the passed in $config 39 | $majorVersion = '4'; 40 | // Dev server container name & port are based on the major version of this plugin 41 | $devPort = 3000 + (int)$majorVersion; 42 | $versionName = 'v' . $majorVersion; 43 | return [ 44 | 'components' => [ 45 | 'ga4' => Ga4::class, 46 | 'commerce' => CommerceService::class, 47 | // Register the vite service 48 | 'vite' => [ 49 | 'assetClass' => InstantAnalyticsAsset::class, 50 | 'checkDevServer' => true, 51 | 'class' => VitePluginService::class, 52 | 'devServerInternal' => 'http://craft-instantanalytics-ga4-' . $versionName . '-buildchain-dev:' . $devPort, 53 | 'devServerPublic' => 'http://localhost:' . $devPort, 54 | 'errorEntry' => 'src/js/app.ts', 55 | 'useDevServer' => true, 56 | ], 57 | ], 58 | ]; 59 | } 60 | 61 | // Public Methods 62 | // ========================================================================= 63 | 64 | /** 65 | * Returns the GA4 service 66 | * 67 | * @return Ga4 The GA4 service 68 | * @throws InvalidConfigException 69 | */ 70 | public function getGa4(): Ga4 71 | { 72 | return $this->get('ga4'); 73 | } 74 | 75 | /** 76 | * Returns the commerce service 77 | * 78 | * @return CommerceService The commerce service 79 | * @throws InvalidConfigException 80 | */ 81 | public function getCommerce(): CommerceService 82 | { 83 | return $this->get('commerce'); 84 | } 85 | 86 | /** 87 | * Returns the vite service 88 | * 89 | * @return VitePluginService The vite service 90 | * @throws InvalidConfigException 91 | */ 92 | public function getVite(): VitePluginService 93 | { 94 | return $this->get('vite'); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/variables/InstantAnalyticsVariable.php: -------------------------------------------------------------------------------- 1 | ga4->getPageViewEvent($url, $title); 51 | } 52 | 53 | /** 54 | * Get a simple event 55 | * 56 | * @param string $eventName 57 | * @return BaseEvent 58 | */ 59 | public function simpleEvent(string $eventName = ''): BaseEvent 60 | { 61 | return InstantAnalytics::$plugin->ga4->getSimpleEvent($eventName); 62 | } 63 | 64 | /** 65 | * Return the GA4 Analytics object 66 | * 67 | * @return Analytics 68 | */ 69 | public function ga4(): Analytics 70 | { 71 | return InstantAnalytics::$plugin->ga4->getAnalytics(); 72 | } 73 | 74 | /** 75 | * @param Product|Variant $productVariant the Product or Variant 76 | */ 77 | public function addCommerceProductView($productVariant): void 78 | { 79 | InstantAnalytics::$plugin->commerce->addCommerceProductImpression($productVariant); 80 | } 81 | 82 | /** 83 | * Get a PageView tracking URL 84 | * 85 | * @param $url 86 | * @param $title 87 | * 88 | * @return Markup 89 | * @throws Exception 90 | */ 91 | public function pageViewTrackingUrl($url, $title): Markup 92 | { 93 | return Template::raw(AnalyticsHelper::getPageViewTrackingUrl($url, $title)); 94 | } 95 | 96 | /** 97 | * Get an Event tracking URL 98 | * 99 | * @param string $url 100 | * @param string $eventName 101 | * @param array $params 102 | * @return Markup 103 | * @throws Exception 104 | */ 105 | public function eventTrackingUrl( 106 | string $url, 107 | string $eventName = '', 108 | array $params = [], 109 | ): Markup { 110 | return Template::raw(AnalyticsHelper::getEventTrackingUrl($url, $eventName, $params)); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/ga4/ComponentFactory.php: -------------------------------------------------------------------------------- 1 | ItemParameter::class, 64 | 'AddPaymentInfoEvent' => AddPaymentInfoEvent::class, 65 | 'AddShippingInfoEvent' => AddShippingInfoEvent::class, 66 | 'AddToCartEvent' => AddToCartEvent::class, 67 | 'BaseEvent' => BaseEvent::class, 68 | 'BeginCheckoutEvent' => BeginCheckoutEvent::class, 69 | 'LoginEvent' => LoginEvent::class, 70 | 'PageViewEvent' => PageViewEvent::class, 71 | 'PurchaseEvent' => PurchaseEvent::class, 72 | 'RefundEvent' => RefundEvent::class, 73 | 'RemoveFromCartEvent' => RemoveFromCartEvent::class, 74 | 'SearchEvent' => SearchEvent::class, 75 | 'SelectItemEvent' => SelectItemEvent::class, 76 | 'SignUpEvent' => SignUpEvent::class, 77 | 'ViewCartEvent' => ViewCartEvent::class, 78 | 'ViewItemEvent' => ViewItemEvent::class, 79 | 'ViewItemListEvent' => ViewItemListEvent::class, 80 | 'ViewSearchResultsEvent' => ViewSearchResultsEvent::class, 81 | ]; 82 | 83 | if (!array_key_exists($componentName, $componentMap)) { 84 | throw new \InvalidArgumentException(Craft::t('instant-analytics-ga4', 'Unknown event type - ' . $componentName)); 85 | } 86 | 87 | return new $componentMap[$componentName](); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/services/Ga4.php: -------------------------------------------------------------------------------- 1 | _analytics) { 45 | $this->_analytics = Craft::createObject(Analytics::class); 46 | $this->_analytics->init(); 47 | } 48 | 49 | return $this->_analytics; 50 | } 51 | 52 | /** 53 | * Send a page view event 54 | */ 55 | public function addPageViewEvent(string $url = '', string $pageTitle = ''): void 56 | { 57 | $request = Craft::$app->getRequest(); 58 | 59 | if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest() && !$this->_pageViewSent) { 60 | $this->_pageViewSent = true; 61 | 62 | $pageView = $this->getPageViewEvent($url, !empty($pageTitle) ? $pageTitle : InstantAnalytics::$currentTemplate); 63 | $this->getAnalytics()->addEvent($pageView); 64 | 65 | InstantAnalytics::$plugin->logAnalyticsEvent( 66 | 'pageView event queued for sending', 67 | [], 68 | __METHOD__ 69 | ); 70 | } 71 | } 72 | 73 | /** 74 | * Add a basic event to be sent to GA4 75 | * 76 | * @param string $url 77 | * @param string $eventName 78 | * @param array $params 79 | */ 80 | public function addSimpleEvent(string $url, string $eventName, array $params): void 81 | { 82 | $baseEvent = $this->getSimpleEvent($eventName); 83 | $baseEvent->setParamValue('documentPath', parse_url($url, PHP_URL_PATH)); 84 | 85 | foreach ($params as $param => $value) { 86 | $baseEvent->addParam($param, new BaseParameter($value)); 87 | } 88 | 89 | $this->getAnalytics()->addEvent($baseEvent); 90 | 91 | InstantAnalytics::$plugin->logAnalyticsEvent( 92 | 'Simple event queued for {url} with the following parameters {params}', 93 | ['url' => $url, 'params' => Json::encode($params)], 94 | __METHOD__ 95 | ); 96 | } 97 | 98 | /** 99 | * Create a page view event 100 | * 101 | * @param string $url 102 | * @param string $pageTitle 103 | * @return PageViewEvent 104 | */ 105 | public function getPageViewEvent(string $url = '', string $pageTitle = ''): PageViewEvent 106 | { 107 | $event = $this->getAnalytics()->create()->PageViewEvent(); 108 | $event->setPageTitle($pageTitle); 109 | 110 | // If SEOmatic is installed, set the page title from it 111 | $seomaticTitle = AnalyticsHelper::getTitleFromSeomatic(); 112 | 113 | if ($seomaticTitle) { 114 | $event->setPageTitle($seomaticTitle); 115 | } 116 | 117 | $event->setPageLocation(AnalyticsHelper::getDocumentPathFromUrl($url)); 118 | 119 | return $event; 120 | } 121 | 122 | /** 123 | * Create a simple event 124 | * 125 | * @param string $eventName 126 | * @return BaseEvent 127 | */ 128 | public function getSimpleEvent(string $eventName): BaseEvent 129 | { 130 | $baseEvent = $this->getAnalytics()->create()->BaseEvent(); 131 | $baseEvent->setName($eventName); 132 | 133 | return $baseEvent; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/config.php: -------------------------------------------------------------------------------- 1 | '', 30 | 31 | /** 32 | * The default Google Analytics measurement API secret used by GA4. 33 | */ 34 | 'googleAnalyticsMeasurementApiSecret' => '', 35 | 36 | /** 37 | * Should the query string be stripped from the page tracking URL? 38 | */ 39 | 'stripQueryString' => true, 40 | 41 | /** 42 | * Should page views be sent automatically when a page view happens? 43 | */ 44 | 'autoSendPageView' => true, 45 | 46 | /** 47 | * If you plan to use Instant Analytics in conjunction with frontend JavaScript, this setting should be on, so that Instant Analytics requires a `clientId` from the frontend-set GA cookie before it will send analytics data. 48 | */ 49 | 'requireGaCookieClientId' => true, 50 | 51 | /** 52 | * Should the GCLID cookie be created if it doesn't exist? 53 | */ 54 | 'createGclidCookie' => true, 55 | 56 | /** 57 | * The field in a Commerce Product Variant that should be used for the category 58 | */ 59 | 'productCategoryField' => '', 60 | 61 | /** 62 | * The field in a Commerce Product Variant that should be used for the brand 63 | */ 64 | 'productBrandField' => '', 65 | 66 | /** 67 | * Whether add to cart events should be automatically sent 68 | */ 69 | 'autoSendAddToCart' => true, 70 | 71 | /** 72 | * Whether remove from cart events should be automatically sent 73 | * 74 | * @var bool 75 | */ 76 | 'autoSendRemoveFromCart' => true, 77 | 78 | /** 79 | * Whether purchase complete events should be automatically sent 80 | */ 81 | 'autoSendPurchaseComplete' => true, 82 | 83 | /** 84 | * Controls whether Instant Analytics will send analytics data. 85 | */ 86 | 'sendAnalyticsData' => true, 87 | 88 | /** 89 | * Controls whether Instant Analytics will send analytics data when `devMode` is on. 90 | */ 91 | 'sendAnalyticsInDevMode' => true, 92 | 93 | /** 94 | * Controls whether we should filter out bot UserGents. 95 | */ 96 | 'filterBotUserAgents' => true, 97 | 98 | /** 99 | * Controls whether we should exclude users logged into an admin account from Analytics tracking. 100 | */ 101 | 'adminExclude' => false, 102 | 103 | /** 104 | * Controls whether analytics that blocked from being sent should be logged to 105 | * storage/logs/web.log 106 | * These are always logged if `devMode` is on 107 | */ 108 | 'logExcludedAnalytics' => true, 109 | 110 | /** 111 | * Contains an array of Craft user group handles to exclude from Analytics tracking. If there's a match 112 | * for any of them, analytics data is not sent. 113 | */ 114 | 'groupExcludes' => array( 115 | 'some_user_group_handle', 116 | ), 117 | 118 | /** 119 | * Contains an array of keys that correspond to $_SERVER[] super-global array keys to test against. 120 | * Each item in the sub-array is tested against the $_SERVER[] super-global key via RegEx; if there's 121 | * a match for any of them, analytics data is not sent. This allows you to filter based on whatever 122 | * information you want. 123 | * Reference: http://php.net/manual/en/reserved.variables.server.php 124 | * RegEx tester: http://regexr.com 125 | */ 126 | 'serverExcludes' => array( 127 | 'REMOTE_ADDR' => array( 128 | '/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/', 129 | ), 130 | ), 131 | ]; 132 | -------------------------------------------------------------------------------- /src/twigextensions/InstantAnalyticsTwigExtension.php: -------------------------------------------------------------------------------- 1 | getView(); 57 | if ($view->getIsRenderingPageTemplate()) { 58 | $request = Craft::$app->getRequest(); 59 | if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) { 60 | // Return our Analytics object as a Twig global 61 | $globals = [ 62 | 'instantAnalytics' => InstantAnalytics::$plugin->ga4->getAnalytics(), 63 | ]; 64 | } 65 | } 66 | 67 | return $globals; 68 | } 69 | 70 | /** 71 | * @inheritdoc 72 | */ 73 | public function getFilters(): array 74 | { 75 | return [ 76 | new TwigFilter('pageViewEvent', [$this, 'pageViewEvent']), 77 | new TwigFilter('simpleAnalyticsEvent', [$this, 'simpleEvent']), 78 | new TwigFilter('pageViewTrackingUrl', [$this, 'pageViewTrackingUrl']), 79 | new TwigFilter('eventTrackingUrl', [$this, 'eventTrackingUrl']), 80 | ]; 81 | } 82 | 83 | /** 84 | * @inheritdoc 85 | */ 86 | public function getFunctions(): array 87 | { 88 | return [ 89 | new TwigFunction('pageViewEvent', [$this, 'pageViewEvent']), 90 | new TwigFunction('simpleAnalyticsEvent', [$this, 'simpleEvent']), 91 | new TwigFunction('pageViewTrackingUrl', [$this, 'pageViewTrackingUrl']), 92 | new TwigFunction('eventTrackingUrl', [$this, 'eventTrackingUrl']), 93 | ]; 94 | } 95 | 96 | /** 97 | * Get a PageView analytics object 98 | * 99 | * @param string $url 100 | * @param string $title 101 | * 102 | * @return PageViewEvent object 103 | */ 104 | public function pageViewEvent(string $url = '', string $title = ''): PageViewEvent 105 | { 106 | return InstantAnalytics::$plugin->ga4->getPageViewEvent($url, $title); 107 | } 108 | 109 | /** 110 | * Get an Event analytics object 111 | * 112 | * @param string $eventName 113 | * @return BaseEvent 114 | */ 115 | public function simpleEvent(string $eventName = ''): BaseEvent 116 | { 117 | return InstantAnalytics::$plugin->ga4->getSimpleEvent($eventName); 118 | } 119 | 120 | /** 121 | * Get a PageView tracking URL 122 | * 123 | * @param $url 124 | * @param $title 125 | * 126 | * @return Markup 127 | * @throws Exception 128 | */ 129 | public function pageViewTrackingUrl($url, $title): Markup 130 | { 131 | return Template::raw(Analytics::getPageViewTrackingUrl($url, $title)); 132 | } 133 | 134 | /** 135 | * Get an Event tracking URL 136 | * 137 | * @param string $url 138 | * @param string $eventName 139 | * @param array $params 140 | * @return Markup 141 | * @throws Exception 142 | */ 143 | public function eventTrackingUrl( 144 | string $url, 145 | string $eventName, 146 | array $params = [], 147 | ): Markup { 148 | return Template::raw(Analytics::getEventTrackingUrl($url, $eventName, $params)); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/templates/settings.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * Instant Analytics plugin for Craft CMS 4 | * 5 | * Instant Analytics Settings.twig 6 | * 7 | * @author nystudio107 8 | * @copyright Copyright (c) 2017 nystudio107 9 | * @link https://nystudio107.com 10 | * @package InstantAnalytics 11 | * @since 1.0.0 12 | */ 13 | #} 14 | 15 | {% import "_includes/forms" as forms %} 16 | 17 | {% do view.registerAssetBundle("nystudio107\\instantanalyticsGa4\\assetbundles\\instantanalytics\\InstantAnalyticsAsset") %} 18 | 19 | {% set commerce = craft.app.plugins.getPlugin('commerce', false) %} 20 | {% set commerceInstalled = craft.app.plugins.isPluginInstalled('commerce') %} 21 | {% set commerceEnabled = craft.app.plugins.isPluginEnabled('commerce') %} 22 | 23 | {% set commerceEnabled = (commerce and commerceEnabled and commerceInstalled) %} 24 | 25 | {{ forms.autosuggestField({ 26 | label: 'Google Analytics Measurement ID:', 27 | instructions: "Enter your Google Analytics Measurement ID here to be used by the GA4 API. Only enter the ID, e.g.: G-XXXXXXXXXX, not the entire script code.", 28 | suggestEnvVars: true, 29 | id: 'googleAnalyticsMeasurementId', 30 | name: 'googleAnalyticsMeasurementId', 31 | value: settings['googleAnalyticsMeasurementId'], 32 | }) }} 33 | 34 | {{ forms.autosuggestField({ 35 | label: 'Google Analytics Measurement API Secret:', 36 | instructions: "Enter your Google Analytics Measurement API secret here, to be used by GA4 API.", 37 | suggestEnvVars: true, 38 | id: 'googleAnalyticsMeasurementApiSecret', 39 | name: 'googleAnalyticsMeasurementApiSecret', 40 | value: settings['googleAnalyticsMeasurementApiSecret'], 41 | }) }} 42 | 43 | {{ forms.lightswitchField({ 44 | label: 'Strip Query String from PageView URLs:', 45 | instructions: "If this setting is on, the query string will be stripped from PageView URLs before being sent to Google Analytics. e.g.: `/some/path?token=1235312` would be sent as just `/some/path`", 46 | id: 'stripQueryString', 47 | name: 'stripQueryString', 48 | on: settings['stripQueryString']}) }} 49 | 50 | {{ forms.lightswitchField({ 51 | label: 'Auto Send PageViews:', 52 | instructions: "If this setting is on, a PageView will automatically be sent to Google after a every page is rendered. If it is off, you'll need to send it manually using `{% hook 'iaSendPageView' %}`", 53 | id: 'autoSendPageView', 54 | name: 'autoSendPageView', 55 | on: settings['autoSendPageView']}) }} 56 | 57 | {{ forms.lightswitchField({ 58 | label: 'Create GCLID Cookie:', 59 | instructions: "Google Click Identifier (GCLID), is a unique tracking parameter that Google uses to transfer information between your Google Ads account and your Google Analytics account. If this setting is on, the GCLID will be created if it doesn't exist for the current request.", 60 | id: 'createGclidCookie', 61 | name: 'createGclidCookie', 62 | on: settings['createGclidCookie']}) }} 63 | 64 | {{ forms.lightswitchField({ 65 | label: 'Auto Send "Add To Cart" Events:', 66 | instructions: "If this setting is on, Google Analytics Enhanced Ecommerce events are automatically sent when an item is added to your Craft Commerce cart.", 67 | id: 'autoSendAddToCart', 68 | name: 'autoSendAddToCart', 69 | disabled: (not commerceEnabled), 70 | on: settings['autoSendAddToCart']}) }} 71 | 72 | {{ forms.lightswitchField({ 73 | label: 'Auto Send "Remove From Cart" Events:', 74 | instructions: "If this setting is on, Google Analytics Enhanced Ecommerce events are automatically sent when an item is removed from your Craft Commerce cart.", 75 | id: 'autoSendRemoveFromCart', 76 | name: 'autoSendRemoveFromCart', 77 | disabled: (not commerceEnabled), 78 | on: settings['autoSendRemoveFromCart']}) }} 79 | 80 | {{ forms.lightswitchField({ 81 | label: 'Auto Send "Purchase Complete" Events:', 82 | instructions: "If this setting is on, Google Analytics Enhanced Ecommerce events are automatically sent a purchase is completed.", 83 | id: 'autoSendPurchaseComplete', 84 | name: 'autoSendPurchaseComplete', 85 | disabled: (not commerceEnabled), 86 | on: settings['autoSendPurchaseComplete']}) }} 87 | 88 | {{ forms.selectField({ 89 | label: 'Commerce Product Category Field:', 90 | instructions: "Choose the field in your Product or Variant field layout that should be used for the product's Category field for Google Analytics Enhanced Ecommerce", 91 | id: 'productCategoryField', 92 | name: 'productCategoryField', 93 | options: commerceFields, 94 | disabled: (not commerceEnabled), 95 | value: settings['productCategoryField'], 96 | }) }} 97 | 98 | {{ forms.selectField({ 99 | label: 'Commerce Product Brand Field:', 100 | instructions: "Choose the field in your Product or Variant field layout that should be used for the product's Brand field for Google Analytics Enhanced Ecommerce", 101 | id: 'productBrandField', 102 | name: 'productBrandField', 103 | options: commerceFields, 104 | disabled: (not commerceEnabled), 105 | value: settings['productBrandField'], 106 | }) }} 107 | -------------------------------------------------------------------------------- /src/models/Settings.php: -------------------------------------------------------------------------------- 1 | [ 179 | '/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/', 180 | ], 181 | ]; 182 | 183 | // Public Methods 184 | // ========================================================================= 185 | 186 | /** 187 | * @return array 188 | */ 189 | public function defineRules(): array 190 | { 191 | return [ 192 | [ 193 | [ 194 | 'stripQueryString', 195 | 'autoSendPageView', 196 | 'requireGaCookieClientId', 197 | 'createGclidCookie', 198 | 'autoSendAddToCart', 199 | 'autoSendRemoveFromCart', 200 | 'autoSendPurchaseComplete', 201 | 'sendAnalyticsData', 202 | 'sendAnalyticsInDevMode', 203 | 'filterBotUserAgents', 204 | 'adminExclude', 205 | 'logExcludedAnalytics', 206 | ], 207 | 'boolean', 208 | ], 209 | [ 210 | [ 211 | 'googleAnalyticsTracking', 212 | 'productCategoryField', 213 | 'productBrandField', 214 | 'googleAnalyticsTracking', 215 | ], 216 | 'string', 217 | ], 218 | [ 219 | [ 220 | 'groupExcludes', 221 | 'serverExcludes', 222 | ], 223 | ArrayValidator::class, 224 | ], 225 | ]; 226 | } 227 | 228 | /** 229 | * @return array 230 | */ 231 | public function behaviors(): array 232 | { 233 | return [ 234 | 'typecast' => [ 235 | 'class' => AttributeTypecastBehavior::class, 236 | // 'attributeTypes' will be composed automatically according to `rules()` 237 | ], 238 | 'parser' => [ 239 | 'class' => EnvAttributeParserBehavior::class, 240 | 'attributes' => [ 241 | 'googleAnalyticsTracking', 242 | ], 243 | ], 244 | ]; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/helpers/Field.php: -------------------------------------------------------------------------------- 1 | [ 48 | CKEditorField::class, 49 | PlainTextField::class, 50 | RedactorField::class, 51 | TagsField::class, 52 | CategoriesField::class, 53 | EntriesField::class, 54 | ], 55 | self::ASSET_FIELD_CLASS_KEY => [ 56 | AssetsField::class, 57 | ], 58 | self::BLOCK_FIELD_CLASS_KEY => [ 59 | MatrixField::class, 60 | ], 61 | ]; 62 | 63 | // Static Methods 64 | // ========================================================================= 65 | 66 | /** 67 | * Return all the fields from the $layout that are of the type 68 | * $fieldClassKey 69 | * 70 | * @param string $fieldClassKey 71 | * @param FieldLayout $layout 72 | * @param bool $keysOnly 73 | * 74 | * @return array 75 | */ 76 | public static function fieldsOfTypeFromLayout( 77 | string $fieldClassKey, 78 | FieldLayout $layout, 79 | bool $keysOnly = true, 80 | ): array { 81 | $foundFields = []; 82 | if (!empty(self::FIELD_CLASSES[$fieldClassKey])) { 83 | $fieldClasses = self::FIELD_CLASSES[$fieldClassKey]; 84 | $fields = $layout->getCustomFields(); 85 | /** @var BaseField $field */ 86 | foreach ($fields as $field) { 87 | /** @var array $fieldClasses */ 88 | foreach ($fieldClasses as $fieldClass) { 89 | if ($field instanceof $fieldClass) { 90 | $foundFields[$field->handle] = $field->name; 91 | } 92 | } 93 | } 94 | } 95 | 96 | // Return only the keys if asked 97 | if ($keysOnly) { 98 | $foundFields = array_keys($foundFields); 99 | } 100 | 101 | return $foundFields; 102 | } 103 | 104 | /** 105 | * Return all of the fields in the $element of the type $fieldClassKey 106 | * 107 | * @param Element $element 108 | * @param string $fieldClassKey 109 | * @param bool $keysOnly 110 | * 111 | * @return array 112 | */ 113 | public static function fieldsOfTypeFromElement( 114 | Element $element, 115 | string $fieldClassKey, 116 | bool $keysOnly = true, 117 | ): array { 118 | $foundFields = []; 119 | $layout = $element->getFieldLayout(); 120 | if ($layout !== null) { 121 | $foundFields = self::fieldsOfTypeFromLayout($fieldClassKey, $layout, $keysOnly); 122 | } 123 | 124 | return $foundFields; 125 | } 126 | 127 | /** 128 | * Return all of the fields from Users layout of the type $fieldClassKey 129 | * 130 | * @param string $fieldClassKey 131 | * @param bool $keysOnly 132 | * 133 | * @return array 134 | */ 135 | public static function fieldsOfTypeFromUsers(string $fieldClassKey, bool $keysOnly = true): array 136 | { 137 | $layout = Craft::$app->getFields()->getLayoutByType(User::class); 138 | 139 | return self::fieldsOfTypeFromLayout($fieldClassKey, $layout, $keysOnly); 140 | } 141 | 142 | /** 143 | * Return all the fields from all Asset Volume layouts of the type 144 | * $fieldClassKey 145 | * 146 | * @param string $fieldClassKey 147 | * @param bool $keysOnly 148 | * 149 | * @return array 150 | */ 151 | public static function fieldsOfTypeFromAssetVolumes(string $fieldClassKey, bool $keysOnly = true): array 152 | { 153 | $foundFields = []; 154 | $volumes = Craft::$app->getVolumes()->getAllVolumes(); 155 | foreach ($volumes as $volume) { 156 | /** @var Volume $volume */ 157 | try { 158 | $layout = $volume->getFieldLayout(); 159 | } catch (Exception $e) { 160 | $layout = null; 161 | } 162 | if ($layout) { 163 | /** @noinspection SlowArrayOperationsInLoopInspection */ 164 | $foundFields = array_merge( 165 | $foundFields, 166 | self::fieldsOfTypeFromLayout($fieldClassKey, $layout, $keysOnly) 167 | ); 168 | } 169 | } 170 | 171 | return $foundFields; 172 | } 173 | 174 | /** 175 | * Return all the fields from all Global Set layouts of the type 176 | * $fieldClassKey 177 | * 178 | * @param string $fieldClassKey 179 | * @param bool $keysOnly 180 | * 181 | * @return array 182 | */ 183 | public static function fieldsOfTypeFromGlobals(string $fieldClassKey, bool $keysOnly = true): array 184 | { 185 | $foundFields = []; 186 | $globals = Craft::$app->getGlobals()->getAllSets(); 187 | foreach ($globals as $global) { 188 | $layout = $global->getFieldLayout(); 189 | /** @phpstan-ignore-next-line */ 190 | if ($layout) { 191 | $fields = self::fieldsOfTypeFromLayout($fieldClassKey, $layout, $keysOnly); 192 | // Prefix the keys with the global set name 193 | $prefix = $global->handle; 194 | $fields = array_combine( 195 | array_map(static function($key) use ($prefix) { 196 | return $prefix . '.' . $key; 197 | }, array_keys($fields)), 198 | $fields 199 | ); 200 | // Merge with any fields we've already found 201 | /** @noinspection SlowArrayOperationsInLoopInspection */ 202 | $foundFields = array_merge( 203 | $foundFields, 204 | $fields 205 | ); 206 | } 207 | } 208 | 209 | return $foundFields; 210 | } 211 | 212 | /** 213 | * Return all of the fields in the $matrixEntry of the type $fieldType class 214 | * 215 | * @param Entry $matrixEntry 216 | * @param string $fieldType 217 | * @param bool $keysOnly 218 | * 219 | * @return array 220 | */ 221 | public static function matrixFieldsOfType(Entry $matrixEntry, string $fieldType, bool $keysOnly = true): array 222 | { 223 | $foundFields = []; 224 | 225 | try { 226 | $matrixEntryTypeModel = $matrixEntry->getType(); 227 | } catch (InvalidConfigException $e) { 228 | $matrixEntryTypeModel = null; 229 | } 230 | if ($matrixEntryTypeModel) { 231 | $fields = $matrixEntryTypeModel->getCustomFields(); 232 | /** @var BaseField $field */ 233 | foreach ($fields as $field) { 234 | if ($field instanceof $fieldType) { 235 | $foundFields[$field->handle] = $field->name; 236 | } 237 | } 238 | // Return only the keys if asked 239 | if ($keysOnly) { 240 | $foundFields = array_keys($foundFields); 241 | } 242 | } 243 | 244 | return $foundFields; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/helpers/Analytics.php: -------------------------------------------------------------------------------- 1 | renderEnabled) { 40 | return null; 41 | } 42 | $titleTag = Seomatic::$plugin->title->get('title'); 43 | 44 | if ($titleTag === null) { 45 | return null; 46 | } 47 | 48 | $titleArray = $titleTag->renderAttributes(); 49 | 50 | if (empty($titleArray['title'])) { 51 | return null; 52 | } 53 | 54 | return $titleArray['title']; 55 | } 56 | 57 | /** 58 | * Return a sanitized documentPath from a URL 59 | * 60 | * @param string $url 61 | * 62 | * @return string 63 | */ 64 | public static function getDocumentPathFromUrl(string $url = ''): string 65 | { 66 | if ($url === '') { 67 | $url = Craft::$app->getRequest()->getFullPath(); 68 | } 69 | 70 | // We want to send just a path to GA for page views 71 | if (UrlHelper::isAbsoluteUrl($url)) { 72 | $urlParts = parse_url($url); 73 | $url = $urlParts['path'] ?? '/'; 74 | if (isset($urlParts['query'])) { 75 | $url .= '?' . $urlParts['query']; 76 | } 77 | } 78 | 79 | // We don't want to send protocol-relative URLs either 80 | if (UrlHelper::isProtocolRelativeUrl($url)) { 81 | $url = substr($url, 1); 82 | } 83 | 84 | // Strip the query string if that's the global config setting 85 | if (InstantAnalytics::$settings) { 86 | if (InstantAnalytics::$settings->stripQueryString !== null 87 | && InstantAnalytics::$settings->stripQueryString) { 88 | $url = UrlHelper::stripQueryString($url); 89 | } 90 | } 91 | 92 | // We always want the path to be / rather than empty 93 | if ($url === '') { 94 | $url = '/'; 95 | } 96 | 97 | return $url; 98 | } 99 | 100 | /** 101 | * Get a PageView tracking URL 102 | * 103 | * @param $url 104 | * @param $title 105 | * 106 | * @return string 107 | * @throws Exception 108 | */ 109 | public static function getPageViewTrackingUrl($url, $title): string 110 | { 111 | $urlParams = compact('url', 'title'); 112 | 113 | $path = parse_url($url, PHP_URL_PATH); 114 | $pathFragments = explode('/', rtrim($path, '/')); 115 | $fileName = end($pathFragments); 116 | $trackingUrl = UrlHelper::siteUrl('instantanalytics/pageViewTrack/' . $fileName, $urlParams); 117 | 118 | InstantAnalytics::$plugin->logAnalyticsEvent( 119 | 'Created pageViewTrackingUrl for: {trackingUrl}', 120 | [ 121 | 'trackingUrl' => $trackingUrl, 122 | ], 123 | __METHOD__ 124 | ); 125 | 126 | return $trackingUrl; 127 | } 128 | 129 | /** 130 | * Get an Event tracking URL 131 | * 132 | * @param string $url 133 | * @param string $eventName 134 | * @param array $params 135 | * @return string 136 | * @throws Exception 137 | */ 138 | public static function getEventTrackingUrl( 139 | string $url, 140 | string $eventName, 141 | array $params = [], 142 | ): string { 143 | $urlParams = compact('url', 'eventName', 'params'); 144 | 145 | $fileName = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_BASENAME); 146 | $trackingUrl = UrlHelper::siteUrl('instantanalytics/eventTrack/' . $fileName, $urlParams); 147 | 148 | InstantAnalytics::$plugin->logAnalyticsEvent( 149 | 'Created eventTrackingUrl for: {trackingUrl}', 150 | [ 151 | 'trackingUrl' => $trackingUrl, 152 | ], 153 | __METHOD__ 154 | ); 155 | 156 | return $trackingUrl; 157 | } 158 | 159 | /** 160 | * _shouldSendAnalytics determines whether we should be sending Google 161 | * Analytics data 162 | * 163 | * @return bool 164 | */ 165 | public static function shouldSendAnalytics(): bool 166 | { 167 | $result = true; 168 | $request = Craft::$app->getRequest(); 169 | 170 | $logExclusion = static function(string $setting) { 171 | if (InstantAnalytics::$settings->logExcludedAnalytics) { 172 | $request = Craft::$app->getRequest(); 173 | $requestIp = $request->getUserIP(); 174 | InstantAnalytics::$plugin->logAnalyticsEvent( 175 | 'Analytics excluded for:: {requestIp} due to: `{setting}`', 176 | compact('requestIp', 'setting'), 177 | __METHOD__ 178 | ); 179 | } 180 | }; 181 | 182 | if (!InstantAnalytics::$settings->sendAnalyticsData) { 183 | $logExclusion('sendAnalyticsData'); 184 | return false; 185 | } 186 | 187 | if (!InstantAnalytics::$settings->sendAnalyticsInDevMode && Craft::$app->getConfig()->getGeneral()->devMode) { 188 | $logExclusion('sendAnalyticsInDevMode'); 189 | return false; 190 | } 191 | 192 | if ($request->getIsConsoleRequest()) { 193 | $logExclusion('Craft::$app->getRequest()->getIsConsoleRequest()'); 194 | return false; 195 | } 196 | 197 | if ($request->getIsCpRequest()) { 198 | $logExclusion('Craft::$app->getRequest()->getIsCpRequest()'); 199 | return false; 200 | } 201 | 202 | if ($request->getIsLivePreview()) { 203 | $logExclusion('Craft::$app->getRequest()->getIsLivePreview()'); 204 | return false; 205 | } 206 | 207 | // Check the $_SERVER[] super-global exclusions 208 | if (InstantAnalytics::$settings->serverExcludes !== null 209 | && !empty(InstantAnalytics::$settings->serverExcludes)) { 210 | foreach (InstantAnalytics::$settings->serverExcludes as $match => $matchArray) { 211 | if (isset($_SERVER[$match])) { 212 | foreach ($matchArray as $matchItem) { 213 | if (preg_match($matchItem, $_SERVER[$match])) { 214 | $logExclusion('serverExcludes'); 215 | 216 | return false; 217 | } 218 | } 219 | } 220 | } 221 | } 222 | 223 | // Filter out bot/spam requests via UserAgent 224 | if (InstantAnalytics::$settings->filterBotUserAgents) { 225 | $crawlerDetect = new CrawlerDetect(); 226 | // Check the user agent of the current 'visitor' 227 | if ($crawlerDetect->isCrawler()) { 228 | $logExclusion('filterBotUserAgents'); 229 | 230 | return false; 231 | } 232 | } 233 | 234 | // Filter by user group 235 | $userService = Craft::$app->getUser(); 236 | /** @var ?UserElement $user */ 237 | $user = $userService->getIdentity(); 238 | if ($user) { 239 | if (InstantAnalytics::$settings->adminExclude && $user->admin) { 240 | $logExclusion('adminExclude'); 241 | 242 | return false; 243 | } 244 | 245 | if (InstantAnalytics::$settings->groupExcludes !== null 246 | && !empty(InstantAnalytics::$settings->groupExcludes)) { 247 | foreach (InstantAnalytics::$settings->groupExcludes as $matchItem) { 248 | if ($user->isInGroup($matchItem)) { 249 | $logExclusion('groupExcludes'); 250 | 251 | return false; 252 | } 253 | } 254 | } 255 | } 256 | 257 | return $result; 258 | } 259 | 260 | /** 261 | * getClientId handles the parsing of the _ga cookie or setting it to a 262 | * unique identifier 263 | * 264 | * @return string the cid 265 | */ 266 | public static function getClientId(): string 267 | { 268 | $cid = ''; 269 | if (isset($_COOKIE['_ga'])) { 270 | $parts = explode(".", $_COOKIE['_ga'], 4); 271 | if ($parts !== false) { 272 | $cid = implode('.', array_slice($parts, 2)); 273 | } 274 | } elseif (isset($_COOKIE['_ia']) && $_COOKIE['_ia'] !== '') { 275 | $cid = $_COOKIE['_ia']; 276 | } else { 277 | // Generate our own client id, otherwise. 278 | $cid = static::gaGenUUID() . '.1'; 279 | } 280 | 281 | if (InstantAnalytics::$settings->createGclidCookie && !empty($cid)) { 282 | setcookie('_ia', $cid, strtotime('+2 years'), '/'); // Two years 283 | } 284 | 285 | return $cid; 286 | } 287 | 288 | /** 289 | * Get the Google Analytics session string from the cookie. 290 | * 291 | * @return string 292 | */ 293 | public static function getSessionString(): string 294 | { 295 | $sessionString = ''; 296 | $measurementId = App::parseEnv(InstantAnalytics::$settings->googleAnalyticsMeasurementId); 297 | $cookieName = '_ga_' . StringHelper::removeLeft($measurementId, 'G-'); 298 | 299 | if (isset($_COOKIE[$cookieName])) { 300 | $parts = explode(".", $_COOKIE[$cookieName], 5); 301 | if ($parts !== false) { 302 | $sessionString = implode('.', array_slice($parts, 2, 2)); 303 | } 304 | } 305 | 306 | return $sessionString; 307 | } 308 | 309 | /** 310 | * Get the user id. 311 | * 312 | * @return string 313 | */ 314 | public static function getUserId(): string 315 | { 316 | $userId = Craft::$app->getUser()->getId(); 317 | 318 | if (!$userId) { 319 | return ''; 320 | } 321 | 322 | return $userId; 323 | } 324 | 325 | /** 326 | * gaGenUUID Generate UUID v4 function - needed to generate a CID when one 327 | * isn't available 328 | * 329 | * @return string The generated UUID 330 | */ 331 | protected static function gaGenUUID() 332 | { 333 | return sprintf( 334 | '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', 335 | // 32 bits for "time_low" 336 | mt_rand(0, 0xffff), 337 | mt_rand(0, 0xffff), 338 | // 16 bits for "time_mid" 339 | mt_rand(0, 0xffff), 340 | // 16 bits for "time_hi_and_version", 341 | // four most significant bits holds version number 4 342 | mt_rand(0, 0x0fff) | 0x4000, 343 | // 16 bits, 8 bits for "clk_seq_hi_res", 344 | // 8 bits for "clk_seq_low", 345 | // two most significant bits holds zero and one for variant DCE1.1 346 | mt_rand(0, 0x3fff) | 0x8000, 347 | // 48 bits for "node" 348 | mt_rand(0, 0xffff), 349 | mt_rand(0, 0xffff), 350 | mt_rand(0, 0xffff) 351 | ); 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/ga4/Analytics.php: -------------------------------------------------------------------------------- 1 | _sessionString === null) { 102 | $this->_sessionString = AnalyticsHelper::getSessionString(); 103 | } 104 | 105 | if (str_contains($this->_sessionString, '.')) { 106 | [$sessionId, $sessionNumber] = explode('.', $this->_sessionString); 107 | $event->setParamValue('sessionId', $sessionId); 108 | $event->setParamValue('sessionNumber', $sessionNumber); 109 | } 110 | 111 | $this->eventList[] = $event; 112 | } 113 | 114 | /** 115 | * Send the events collected so far. 116 | * 117 | * @return ?array 118 | * @throws HydrationException 119 | * @throws ValidationException 120 | */ 121 | public function sendCollectedEvents(): ?array 122 | { 123 | if ($this->_shouldSendAnalytics === null) { 124 | $this->_shouldSendAnalytics = AnalyticsHelper::shouldSendAnalytics(); 125 | } 126 | 127 | if (!$this->_shouldSendAnalytics) { 128 | return null; 129 | } 130 | 131 | $service = $this->service(); 132 | 133 | if (!$service) { 134 | return null; 135 | } 136 | 137 | $request = $this->request(); 138 | $eventCount = count($this->eventList); 139 | 140 | if (!InstantAnalytics::$settings->sendAnalyticsData) { 141 | InstantAnalytics::$plugin->logAnalyticsEvent( 142 | 'Analytics not enabled - skipped sending {count} events', 143 | ['count' => $eventCount], 144 | __METHOD__ 145 | ); 146 | 147 | return null; 148 | } 149 | 150 | if ($eventCount === 0) { 151 | InstantAnalytics::$plugin->logAnalyticsEvent( 152 | 'No events collected to send', 153 | [], 154 | __METHOD__ 155 | ); 156 | 157 | return null; 158 | } 159 | 160 | InstantAnalytics::$plugin->logAnalyticsEvent( 161 | 'Sending {count} analytics events', 162 | ['count' => $eventCount], 163 | __METHOD__ 164 | ); 165 | 166 | // Batch into groups of 25 167 | $responses = []; 168 | 169 | foreach (array_chunk($this->eventList, 25) as $chunk) { 170 | $request->getEvents()->setEventList([]); 171 | 172 | /** @var AbstractEvent $event */ 173 | foreach ($chunk as $event) { 174 | $request->addEvent($event); 175 | } 176 | 177 | $responses[] = $service->send($request); 178 | } 179 | 180 | 181 | return $responses; 182 | } 183 | 184 | public function getAffiliation(): ?string 185 | { 186 | return $this->_affiliation; 187 | } 188 | 189 | /** 190 | * Set affiliation for all the events that incorporate Commerce Product info for the remaining duration of request. 191 | * 192 | * @param string $affiliation 193 | * @return $this 194 | */ 195 | public function setAffiliation(string $affiliation): self 196 | { 197 | $this->_affiliation = $affiliation; 198 | return $this; 199 | } 200 | 201 | /** 202 | * Add a commerce item list impression. 203 | * 204 | * @param Product|Variant $productVariant 205 | * @param int $index 206 | * @param string $listName 207 | * @throws InvalidConfigException 208 | */ 209 | public function addCommerceProductImpression(Product|Variant $productVariant, int $index = 0, string $listName = 'default') 210 | { 211 | InstantAnalytics::$plugin->commerce->addCommerceProductImpression($productVariant); 212 | } 213 | 214 | /** 215 | * Begin checkout. 216 | * 217 | * @param Order $cart 218 | */ 219 | public function beginCheckout(Order $cart) 220 | { 221 | InstantAnalytics::$plugin->commerce->triggerBeginCheckoutEvent($cart); 222 | } 223 | 224 | /** 225 | * Add a commerce item list impression. 226 | * 227 | * @param Product|Variant $productVariant 228 | * @param int $index 229 | * @param string $listName 230 | * @throws InvalidConfigException 231 | * @deprecated `Analytics::addCommerceProductDetailView()` is deprecated. Use `Analytics::addCommerceProductImpression()` instead. 232 | */ 233 | public function addCommerceProductDetailView(Product|Variant $productVariant, int $index = 0, string $listName = 'default') 234 | { 235 | Craft::$app->getDeprecator()->log('Analytics::addCommerceProductDetailView()', '`Analytics::addCommerceProductDetailView()` is deprecated. Use `Analytics::addCommerceProductImpression()` instead.'); 236 | $this->addCommerceProductImpression($productVariant); 237 | } 238 | 239 | /** 240 | * Add a commerce product list impression. 241 | * 242 | * @param array $products 243 | * @param $listName 244 | */ 245 | public function addCommerceProductListImpression(array $products, string $listName = 'default') 246 | { 247 | InstantAnalytics::$plugin->commerce->addCommerceProductListImpression($products, $listName); 248 | } 249 | 250 | /** 251 | * Set the measurement id. 252 | * 253 | * @param string $measurementId 254 | * @return $this 255 | */ 256 | public function setMeasurementId(string $measurementId): self 257 | { 258 | $this->service()?->setMeasurementId($measurementId); 259 | return $this; 260 | } 261 | 262 | /** 263 | * Set the API secret. 264 | * 265 | * @param string $apiSecret 266 | * @return $this 267 | */ 268 | public function setApiSecret(string $apiSecret): self 269 | { 270 | $this->service()?->setApiSecret($apiSecret); 271 | return $this; 272 | } 273 | 274 | public function __call(string $methodName, array $arguments): ?self 275 | { 276 | $knownProperties = [ 277 | 'allowGoogleSignals' => 'allow_google_signals', 278 | 'allowAdPersonalizationSignals' => 'allow_ad_personalization_signals', 279 | 'campaignContent' => 'campaign_content', 280 | 'campaignId' => 'campaign_id', 281 | 'campaignMedium' => 'campaign_medium', 282 | 'campaignName' => 'campaign_name', 283 | 'campaignSource' => 'campaign_source', 284 | 'campaignTerm' => 'campaign_term', 285 | 'campaign' => 'campaign', 286 | 'clientId' => 'client_id', 287 | 'contentGroup' => 'content_group', 288 | 'cookieDomain' => 'cookie_domain', 289 | 'cookieExpires' => 'cookie_expires', 290 | 'cookieFlags' => 'cookie_flags', 291 | 'cookiePath' => 'cookie_path', 292 | 'cookiePrefix' => 'cookie_prefix', 293 | 'cookieUpdate' => 'cookie_update', 294 | 'language' => 'language', 295 | 'pageLocation' => 'page_location', 296 | 'pageReferrer' => 'page_referrer', 297 | 'pageTitle' => 'page_title', 298 | 'sendPageView' => 'send_page_view', 299 | 'screenResolution' => 'screen_resolution', 300 | 'userId' => 'user_id', 301 | ]; 302 | 303 | if (str_starts_with($methodName, 'set')) { 304 | $methodName = lcfirst(substr($methodName, 3)); 305 | 306 | $service = $this->service(); 307 | if ($service && !empty($knownProperties[$methodName])) { 308 | $service->setAdditionalQueryParam($knownProperties[$methodName], $arguments[0]); 309 | 310 | return $this; 311 | } 312 | } 313 | 314 | return null; 315 | } 316 | 317 | public function request(): BaseRequest 318 | { 319 | if ($this->_request === null) { 320 | $this->_request = new BaseRequest(); 321 | 322 | $this->_request->setClientId(AnalyticsHelper::getClientId()); 323 | 324 | if (InstantAnalytics::$settings->sendUserId) { 325 | $userId = AnalyticsHelper::getUserId(); 326 | 327 | if ($userId) { 328 | $this->request()->setUserId($userId); 329 | } 330 | } 331 | } 332 | 333 | 334 | return $this->_request; 335 | } 336 | 337 | /** 338 | * Init the service used to send events 339 | */ 340 | public function init(): void 341 | { 342 | $this->service(); 343 | $this->request(); 344 | } 345 | 346 | protected function service(): ?Service 347 | { 348 | if ($this->_service === null) { 349 | $settings = InstantAnalytics::$settings; 350 | $apiSecret = App::parseEnv($settings->googleAnalyticsMeasurementApiSecret); 351 | $measurementId = App::parseEnv($settings->googleAnalyticsMeasurementId); 352 | 353 | if (empty($apiSecret) || empty($measurementId)) { 354 | InstantAnalytics::$plugin->logAnalyticsEvent( 355 | 'API secret or measurement ID not set up for Instant Analytics', 356 | [], 357 | __METHOD__ 358 | ); 359 | $this->_service = false; 360 | 361 | return null; 362 | } 363 | $this->_service = new Service($apiSecret, $measurementId); 364 | 365 | $ga4Client = new HttpClient(); 366 | $ga4Client->setClient(Craft::createGuzzleClient()); 367 | $this->_service->setHttpClient($ga4Client); 368 | 369 | $request = Craft::$app->getRequest(); 370 | try { 371 | $session = Craft::$app->getSession(); 372 | } catch (MissingComponentException $exception) { 373 | $session = null; 374 | } 375 | 376 | $this->setPageReferrer($request->getReferrer()); 377 | 378 | // Load any campaign values from session or request 379 | $campaignParams = [ 380 | 'utm_source' => 'CampaignSource', 381 | 'utm_medium' => 'CampaignMedium', 382 | 'utm_campaign' => 'CampaignName', 383 | 'utm_content' => 'CampaignContent', 384 | 'utm_term' => 'CampaignTerm', 385 | ]; 386 | 387 | // Load them up for GA4 388 | foreach ($campaignParams as $key => $method) { 389 | $value = $request->getParam($key) ?? $session->get($key) ?? null; 390 | $method = 'set' . $method; 391 | 392 | $this->$method($value); 393 | 394 | if ($session && $value) { 395 | $session->set($key, $value); 396 | } 397 | } 398 | 399 | // If SEOmatic is installed, set the affiliation as well 400 | if (InstantAnalytics::$seomaticPlugin && Seomatic::$settings->renderEnabled && Seomatic::$plugin->metaContainers->metaSiteVars !== null) { 401 | $siteName = Seomatic::$plugin->metaContainers->metaSiteVars->siteName; 402 | $this->setAffiliation($siteName); 403 | } 404 | } 405 | 406 | if ($this->_service === false) { 407 | return null; 408 | } 409 | 410 | return $this->_service; 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /src/InstantAnalytics.php: -------------------------------------------------------------------------------- 1 | getSettings(); 130 | self::$settings = $settings; 131 | 132 | // Add in our Craft components 133 | $this->addComponents(); 134 | // Install our global event handlers 135 | $this->installEventListeners(); 136 | 137 | Craft::info( 138 | Craft::t( 139 | 'instant-analytics-ga4', 140 | '{name} plugin loaded', 141 | ['name' => $this->name] 142 | ), 143 | __METHOD__ 144 | ); 145 | } 146 | 147 | /** 148 | * Handle the `{% hook iaSendPageView %}` 149 | */ 150 | public function iaSendPageView(/** @noinspection PhpUnusedParameterInspection */ array &$context = []): string 151 | { 152 | $this->ga4->addPageViewEvent(); 153 | return ''; 154 | } 155 | 156 | /** 157 | * Handle the `{% hook iaInsertGtag %}` 158 | */ 159 | public function iaInsertGtag(/** @noinspection PhpUnusedParameterInspection */ array &$context = []): string 160 | { 161 | $config = []; 162 | 163 | if (self::$settings->sendUserId) { 164 | $userId = Analytics::getUserId(); 165 | if (!empty($userId)) { 166 | $config['user_id'] = $userId; 167 | } 168 | } 169 | 170 | $measurementId = App::parseEnv(self::$settings->googleAnalyticsMeasurementId); 171 | 172 | 173 | $view = Craft::$app->getView(); 174 | $existingMode = $view->getTemplateMode(); 175 | $view->setTemplateMode(View::TEMPLATE_MODE_CP); 176 | $content = $view->renderTemplate('instant-analytics-ga4/_includes/gtag', compact('measurementId', 'config')); 177 | $view->setTemplateMode($existingMode); 178 | 179 | return $content; 180 | } 181 | 182 | public function logAnalyticsEvent(string $message, array $variables = [], string $category = ''): void 183 | { 184 | Craft::info( 185 | Craft::t('instant-analytics-ga4', $message, $variables), 186 | $category 187 | ); 188 | } 189 | 190 | /** 191 | * @inheritdoc 192 | */ 193 | protected function settingsHtml(): ?string 194 | { 195 | $commerceFields = []; 196 | 197 | if (self::$commercePlugin !== null) { 198 | $productTypes = self::$commercePlugin->getProductTypes()->getAllProductTypes(); 199 | 200 | foreach ($productTypes as $productType) { 201 | $productFields = $this->getPullFieldsFromLayoutId($productType->fieldLayoutId); 202 | /** @noinspection SlowArrayOperationsInLoopInspection */ 203 | $commerceFields = array_merge($commerceFields, $productFields); 204 | if ($productType->maxVariants > 1) { 205 | $variantFields = $this->getPullFieldsFromLayoutId($productType->variantFieldLayoutId); 206 | /** @noinspection SlowArrayOperationsInLoopInspection */ 207 | $commerceFields = array_merge($commerceFields, $variantFields); 208 | } 209 | } 210 | } 211 | 212 | // Rend the settings template 213 | try { 214 | return Craft::$app->getView()->renderTemplate( 215 | 'instant-analytics-ga4/settings', 216 | [ 217 | 'settings' => $this->getSettings(), 218 | 'commerceFields' => $commerceFields, 219 | ] 220 | ); 221 | } catch (Exception $exception) { 222 | Craft::error($exception->getMessage(), __METHOD__); 223 | } 224 | 225 | return ''; 226 | } 227 | // Protected Methods 228 | // ========================================================================= 229 | 230 | /** 231 | * Add in our Craft components 232 | */ 233 | protected function addComponents(): void 234 | { 235 | $view = Craft::$app->getView(); 236 | // Add in our Twig extensions 237 | $view->registerTwigExtension(new InstantAnalyticsTwigExtension()); 238 | // Install our template hooks 239 | $view->hook('iaSendPageView', [$this, 'iaSendPageView']); 240 | $view->hook('iaInsertGtag', [$this, 'iaInsertGtag']); 241 | 242 | // Register our variables 243 | Event::on( 244 | CraftVariable::class, 245 | CraftVariable::EVENT_INIT, 246 | function(Event $event): void { 247 | /** @var CraftVariable $variable */ 248 | $variable = $event->sender; 249 | $variable->set('instantAnalytics', [ 250 | 'class' => InstantAnalyticsVariable::class, 251 | 'viteService' => $this->vite, 252 | ]); 253 | } 254 | ); 255 | } 256 | 257 | /** 258 | * Install our event listeners 259 | */ 260 | protected function installEventListeners(): void 261 | { 262 | // Handler: Plugins::EVENT_AFTER_INSTALL_PLUGIN 263 | Event::on( 264 | Plugins::class, 265 | Plugins::EVENT_AFTER_INSTALL_PLUGIN, 266 | function(PluginEvent $event): void { 267 | if ($event->plugin === $this) { 268 | $request = Craft::$app->getRequest(); 269 | if ($request->isCpRequest) { 270 | Craft::$app->getResponse()->redirect(UrlHelper::cpUrl('instant-analytics-ga4/welcome'))->send(); 271 | } 272 | } 273 | } 274 | ); 275 | 276 | // Handler: Plugins::EVENT_AFTER_LOAD_PLUGINS 277 | Event::on( 278 | Plugins::class, 279 | Plugins::EVENT_AFTER_LOAD_PLUGINS, 280 | function() { 281 | // Determine if Craft Commerce is installed & enabled 282 | /** @var Commerce $commercePlugin */ 283 | $commercePlugin = Craft::$app->getPlugins()->getPlugin(self::COMMERCE_PLUGIN_HANDLE); 284 | self::$commercePlugin = $commercePlugin; 285 | // Determine if SEOmatic is installed & enabled 286 | /** @var Seomatic $seomaticPlugin */ 287 | $seomaticPlugin = Craft::$app->getPlugins()->getPlugin(self::SEOMATIC_PLUGIN_HANDLE); 288 | self::$seomaticPlugin = $seomaticPlugin; 289 | 290 | // Make sure to install these only after we definitely know whether other plugins are installed 291 | $request = Craft::$app->getRequest(); 292 | // Install only for non-console site requests 293 | if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) { 294 | $this->installSiteEventListeners(); 295 | } 296 | 297 | // Install only for non-console Control Panel requests 298 | if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) { 299 | $this->installCpEventListeners(); 300 | } 301 | } 302 | ); 303 | } 304 | 305 | /** 306 | * Install site event listeners for site requests only 307 | */ 308 | protected function installSiteEventListeners(): void 309 | { 310 | // Handler: UrlManager::EVENT_REGISTER_SITE_URL_RULES 311 | Event::on( 312 | UrlManager::class, 313 | UrlManager::EVENT_REGISTER_SITE_URL_RULES, 314 | function(RegisterUrlRulesEvent $event): void { 315 | Craft::debug( 316 | 'UrlManager::EVENT_REGISTER_SITE_URL_RULES', 317 | __METHOD__ 318 | ); 319 | // Register our Control Panel routes 320 | $event->rules = array_merge( 321 | $event->rules, 322 | $this->customFrontendRoutes() 323 | ); 324 | } 325 | ); 326 | // Remember the name of the currently rendering template 327 | Event::on( 328 | View::class, 329 | View::EVENT_BEFORE_RENDER_PAGE_TEMPLATE, 330 | static function(TemplateEvent $event): void { 331 | self::$currentTemplate = $event->template; 332 | } 333 | ); 334 | // Send the page-view event. 335 | Event::on( 336 | View::class, 337 | View::EVENT_AFTER_RENDER_PAGE_TEMPLATE, 338 | function(TemplateEvent $event): void { 339 | if (self::$settings->autoSendPageView) { 340 | $request = Craft::$app->getRequest(); 341 | if (!$request->getIsAjax()) { 342 | $this->ga4->addPageViewEvent(); 343 | } 344 | } 345 | } 346 | ); 347 | 348 | // Send the collected events 349 | Event::on( 350 | Response::class, 351 | Response::EVENT_BEFORE_SEND, 352 | function(Event $event): void { 353 | // Initialize this sooner rather than later, since it's possible this will want to tinker with cookies 354 | $this->ga4->getAnalytics(); 355 | } 356 | ); 357 | 358 | // Send the collected events 359 | Event::on( 360 | Response::class, 361 | Response::EVENT_AFTER_SEND, 362 | function(Event $event): void { 363 | $this->ga4->getAnalytics()->sendCollectedEvents(); 364 | } 365 | ); 366 | 367 | // Commerce-specific hooks 368 | if (self::$commercePlugin !== null) { 369 | Event::on(Order::class, Order::EVENT_AFTER_COMPLETE_ORDER, function(Event $e): void { 370 | $order = $e->sender; 371 | if (self::$settings->autoSendPurchaseComplete) { 372 | $this->commerce->triggerOrderCompleteEvent($order); 373 | } 374 | }); 375 | 376 | Event::on(Order::class, Order::EVENT_AFTER_ADD_LINE_ITEM, function(LineItemEvent $e): void { 377 | $lineItem = $e->lineItem; 378 | if (self::$settings->autoSendAddToCart) { 379 | $this->commerce->triggerAddToCartEvent($lineItem); 380 | } 381 | }); 382 | 383 | // Check to make sure Order::EVENT_AFTER_REMOVE_LINE_ITEM is defined 384 | if (defined(Order::class . '::EVENT_AFTER_REMOVE_LINE_ITEM')) { 385 | Event::on(Order::class, Order::EVENT_AFTER_REMOVE_LINE_ITEM, function(LineItemEvent $e): void { 386 | $lineItem = $e->lineItem; 387 | if (self::$settings->autoSendRemoveFromCart) { 388 | $this->commerce->triggerRemoveFromCartEvent($lineItem); 389 | } 390 | }); 391 | } 392 | } 393 | } 394 | 395 | /** 396 | * Install site event listeners for Control Panel requests only 397 | */ 398 | protected function installCpEventListeners(): void 399 | { 400 | } 401 | 402 | /** 403 | * Return the custom frontend routes 404 | * 405 | * @return array 406 | */ 407 | protected function customFrontendRoutes(): array 408 | { 409 | return [ 410 | 'instantanalytics/pageViewTrack' => 411 | 'instant-analytics-ga4/track/track-page-view-url', 412 | 'instantanalytics/eventTrack/?' => 413 | 'instant-analytics-ga4/track/track-event-url', 414 | ]; 415 | } 416 | 417 | /** 418 | * @inheritdoc 419 | */ 420 | protected function createSettingsModel(): ?Model 421 | { 422 | return new Settings(); 423 | } 424 | 425 | // Private Methods 426 | // ========================================================================= 427 | 428 | /** 429 | * @param $layoutId 430 | * 431 | * @return mixed[]|array 432 | */ 433 | private function getPullFieldsFromLayoutId($layoutId): array 434 | { 435 | $result = ['' => 'none']; 436 | if ($layoutId === null) { 437 | return $result; 438 | } 439 | 440 | $fieldLayout = Craft::$app->getFields()->getLayoutById($layoutId); 441 | if ($fieldLayout) { 442 | $result = FieldHelper::fieldsOfTypeFromLayout(FieldHelper::TEXT_FIELD_CLASS_KEY, $fieldLayout, false); 443 | } 444 | 445 | return $result; 446 | } 447 | } 448 | -------------------------------------------------------------------------------- /src/services/Commerce.php: -------------------------------------------------------------------------------- 1 | ga4->getAnalytics()->create()->PurchaseEvent(); 50 | $this->addCommerceOrderToEvent($event, $order); 51 | 52 | InstantAnalytics::$plugin->ga4->getAnalytics()->addEvent($event); 53 | 54 | InstantAnalytics::$plugin->logAnalyticsEvent( 55 | 'Adding `Commerce - Order Complete event`: `{reference}` => `{price}`', 56 | ['reference' => $order->reference, 'price' => $order->totalPrice], 57 | __METHOD__ 58 | ); 59 | } 60 | } 61 | 62 | /** 63 | * Enqueue analytics information for a new checkout flow 64 | * 65 | * @param ?Order $order 66 | */ 67 | public function triggerBeginCheckoutEvent(Order $order = null) 68 | { 69 | if ($order) { 70 | $event = InstantAnalytics::$plugin->ga4->getAnalytics()->create()->BeginCheckoutEvent(); 71 | // First, include the transaction data 72 | $event->setCurrency($order->getPaymentCurrency()) 73 | ->setValue($order->getTotalPrice()); 74 | 75 | // Add each line item in the cart 76 | $index = 1; 77 | foreach ($order->lineItems as $lineItem) { 78 | $this->addProductDataFromLineItem($event, $lineItem, $index); 79 | $index++; 80 | } 81 | 82 | InstantAnalytics::$plugin->ga4->getAnalytics()->addEvent($event); 83 | 84 | InstantAnalytics::$plugin->logAnalyticsEvent( 85 | 'Adding `Commerce - Begin Checkout event``', 86 | [], 87 | __METHOD__ 88 | ); 89 | } 90 | } 91 | 92 | /** 93 | * Send analytics information for the item added to the cart 94 | * 95 | * @param LineItem $lineItem the line item that was added 96 | */ 97 | public function triggerAddToCartEvent(LineItem $lineItem): void 98 | { 99 | $event = InstantAnalytics::$plugin->ga4->getAnalytics()->create()->AddToCartEvent(); 100 | $this->addProductDataFromLineItem($event, $lineItem); 101 | InstantAnalytics::$plugin->ga4->getAnalytics()->addEvent($event); 102 | 103 | InstantAnalytics::$plugin->logAnalyticsEvent( 104 | 'Adding `Commerce - Add to Cart event`: `{title}` => `{quantity}`', 105 | ['title' => $lineItem->purchasable->title ?? $lineItem->getDescription(), 'quantity' => $lineItem->qty], 106 | __METHOD__ 107 | ); 108 | } 109 | 110 | /** 111 | * Send analytics information for the item removed from the cart 112 | * 113 | * @param LineItem $lineItem 114 | */ 115 | public function triggerRemoveFromCartEvent(LineItem $lineItem) 116 | { 117 | $event = InstantAnalytics::$plugin->ga4->getAnalytics()->create()->RemoveFromCartEvent(); 118 | $this->addProductDataFromLineItem($event, $lineItem); 119 | InstantAnalytics::$plugin->ga4->getAnalytics()->addEvent($event); 120 | 121 | InstantAnalytics::$plugin->logAnalyticsEvent( 122 | 'Adding `Commerce - Remove from Cart event`: `{title}` => `{quantity}`', 123 | ['title' => $lineItem->purchasable->title ?? $lineItem->getDescription(), 'quantity' => $lineItem->qty], 124 | __METHOD__ 125 | ); 126 | } 127 | 128 | /** 129 | * Add a product impression from a Craft Commerce Product or Variant 130 | * 131 | * @param Product|Variant|null $productVariant the Product or Variant 132 | * @throws InvalidConfigException 133 | */ 134 | public function addCommerceProductImpression(Variant|Product|null $productVariant): void 135 | { 136 | if ($productVariant) { 137 | $event = InstantAnalytics::$plugin->ga4->getAnalytics()->create()->ViewItemEvent(); 138 | $this->addProductDataFromProductOrVariant($event, $productVariant); 139 | 140 | InstantAnalytics::$plugin->ga4->getAnalytics()->addEvent($event); 141 | 142 | $sku = $productVariant instanceof Product ? $productVariant->getDefaultVariant()->sku : $productVariant->sku; 143 | $name = $productVariant instanceof Product ? $productVariant->getName() : $productVariant->getProduct()->getName(); 144 | InstantAnalytics::$plugin->logAnalyticsEvent( 145 | 'Adding view item event for `{sku}` - `{name}` - `{name}` - `{index}`', 146 | ['sku' => $sku, 'name' => $name], 147 | __METHOD__ 148 | ); 149 | } 150 | } 151 | 152 | /** 153 | * Add a product list impression from a Craft Commerce Product or Variant list 154 | * 155 | * @param Product[]|Variant[] $products 156 | * @param string $listName 157 | */ 158 | public function addCommerceProductListImpression(array $products, string $listName = 'default'): void 159 | { 160 | if (!empty($products)) { 161 | $event = InstantAnalytics::$plugin->ga4->getAnalytics()->create()->ViewItemListEvent(); 162 | foreach ($products as $index => $productVariant) { 163 | $this->addProductDataFromProductOrVariant($event, $productVariant, $index, $listName); 164 | } 165 | 166 | InstantAnalytics::$plugin->ga4->getAnalytics()->addEvent($event); 167 | 168 | InstantAnalytics::$plugin->logAnalyticsEvent( 169 | 'Adding view item list event. Listing {number} of items from the `{listName}` list.', 170 | ['number' => count($products), 'listName' => $listName], 171 | __METHOD__ 172 | ); 173 | } 174 | } 175 | 176 | /** 177 | * Add a Craft Commerce OrderModel to a Purchase Event 178 | * 179 | * @param PurchaseEvent $event The PurchaseEvent 180 | * @param Order $order 181 | */ 182 | protected function addCommerceOrderToEvent(PurchaseEvent $event, Order $order) 183 | { 184 | // First, include the transaction data 185 | $event->setCurrency($order->getPaymentCurrency()) 186 | ->setTransactionId($order->reference) 187 | ->setValue($order->getTotalPrice()) 188 | ->setTax($order->getTotalTax()) 189 | ->setShipping($order->getTotalShippingCost()); 190 | 191 | // Coupon code 192 | if ($order->couponCode) { 193 | $event->setCoupon($order->couponCode); 194 | } 195 | 196 | // Add each line item in the transaction 197 | // Two cases - variant and non variant products 198 | $index = 1; 199 | 200 | foreach ($order->lineItems as $lineItem) { 201 | $this->addProductDataFromLineItem($event, $lineItem, $index); 202 | $index++; 203 | } 204 | } 205 | 206 | /** 207 | * Add a Craft Commerce LineItem to an Analytics object 208 | * 209 | * @param ItemBaseEvent $event 210 | * @param LineItem $lineItem 211 | * @param int $index 212 | * @param string $listName 213 | * 214 | * @return string the title of the product 215 | * @throws InvalidConfigException 216 | */ 217 | protected function addProductDataFromLineItem(ItemBaseEvent $event, LineItem $lineItem, int $index = 0, string $listName = ''): string 218 | { 219 | $eventItem = $this->getNewItemParameter(); 220 | 221 | $product = null; 222 | $purchasable = $lineItem->purchasable; 223 | 224 | /** @phpstan-ignore-next-line */ 225 | if ($purchasable === null) { 226 | $eventItem->setItemName($lineItem->getDescription()); 227 | $eventItem->setItemId($lineItem->getSku()); 228 | } else { 229 | $eventItem->setItemName($purchasable->title ?? $lineItem->getDescription()); 230 | $eventItem->setItemId($purchasable->getSku()); 231 | } 232 | $eventItem->setPrice($lineItem->salePrice); 233 | $eventItem->setQuantity($lineItem->qty); 234 | 235 | // Handle this purchasable being a Variant 236 | if (is_a($purchasable, Variant::class)) { 237 | /** @var Variant $purchasable */ 238 | $product = $purchasable->getProduct(); 239 | $variant = $purchasable; 240 | // Product with variants 241 | $eventItem->setItemName($product->title); 242 | $eventItem->setItemVariant($variant->title); 243 | $eventItem->setItemCategory($product->getType()); 244 | } 245 | 246 | // Handle this purchasable being a Product 247 | if (is_a($purchasable, Product::class)) { 248 | /** @var Product $purchasable */ 249 | $product = $purchasable; 250 | $eventItem->setItemName($product->title); 251 | $eventItem->setItemVariant($product->title); 252 | $eventItem->setItemCategory($product->getType()); 253 | } 254 | 255 | // Handle product lists 256 | if ($index) { 257 | $eventItem->setIndex($index); 258 | } 259 | 260 | if ($listName) { 261 | $eventItem->setItemListName($listName); 262 | } 263 | 264 | // Add in any custom categories/brands that might be set 265 | if (InstantAnalytics::$settings && $product) { 266 | if (isset(InstantAnalytics::$settings['productCategoryField']) 267 | && !empty(InstantAnalytics::$settings['productCategoryField'])) { 268 | $category = $this->pullDataFromField( 269 | $product, 270 | InstantAnalytics::$settings['productCategoryField'] 271 | ); 272 | $eventItem->setItemCategory($category); 273 | } 274 | if (isset(InstantAnalytics::$settings['productBrandField']) 275 | && !empty(InstantAnalytics::$settings['productBrandField'])) { 276 | $brand = $this->pullDataFromField( 277 | $product, 278 | InstantAnalytics::$settings['productBrandField'] 279 | ); 280 | 281 | $eventItem->setItemBrand($brand); 282 | } 283 | } 284 | 285 | //Add each product to the hit to be sent 286 | $event->addItem($eventItem); 287 | 288 | return $eventItem->getItemName(); 289 | } 290 | 291 | /** 292 | * Extract product data from a Craft Commerce Product or Variant 293 | * 294 | * @param Product|Variant|null $productVariant the Product or Variant 295 | * 296 | * @throws InvalidConfigException 297 | */ 298 | protected function addProductDataFromProductOrVariant(ItemBaseEvent $event, $productVariant = null, $index = null, $listName = ''): void 299 | { 300 | if ($productVariant === null) { 301 | return; 302 | } 303 | 304 | $eventItem = $this->getNewItemParameter(); 305 | 306 | $isVariant = $productVariant instanceof Variant; 307 | $variant = $isVariant ? $productVariant : $productVariant->getDefaultVariant(); 308 | 309 | if (!$variant) { 310 | return; 311 | } 312 | 313 | $eventItem->setItemId($variant->sku); 314 | $eventItem->setItemName($variant->title); 315 | $eventItem->setPrice((float)number_format($variant->price, 2, '.', '')); 316 | 317 | $category = ($isVariant ? $variant->getProduct() : $productVariant)->getType()['name']; 318 | 319 | if (InstantAnalytics::$settings) { 320 | if (isset(InstantAnalytics::$settings['productCategoryField']) 321 | && !empty(InstantAnalytics::$settings['productCategoryField'])) { 322 | $category = $this->pullDataFromField( 323 | $productVariant, 324 | InstantAnalytics::$settings['productCategoryField'] 325 | ); 326 | /* @TODO not sure what this even does 327 | * if (empty($productData['category']) && $isVariant) { 328 | * $category = $this->pullDataFromField( 329 | * $productVariant->product, 330 | * InstantAnalytics::$settings['productCategoryField'] 331 | * ); 332 | * } 333 | */ 334 | } 335 | $eventItem->setItemCategory($category); 336 | 337 | if (isset(InstantAnalytics::$settings['productBrandField']) 338 | && !empty(InstantAnalytics::$settings['productBrandField'])) { 339 | $brand = $this->pullDataFromField( 340 | $productVariant, 341 | InstantAnalytics::$settings['productBrandField'], 342 | true 343 | ); 344 | /* @TODO not sure what this even does 345 | * if (empty($productData['brand']) && $isVariant) { 346 | * $brand = $this->pullDataFromField( 347 | * $productVariant, 348 | * InstantAnalytics::$settings['productBrandField'], 349 | * true 350 | * ); 351 | * } 352 | */ 353 | $eventItem->setItemBrand($brand); 354 | } 355 | } 356 | 357 | if ($index !== null) { 358 | $eventItem->setIndex($index); 359 | } 360 | 361 | if (!empty($listName)) { 362 | $eventItem->setItemListName($listName); 363 | } 364 | 365 | // Add item info to the event 366 | $event->addItem($eventItem); 367 | } 368 | 369 | /** 370 | * @param Product|Variant|null $productVariant 371 | * @param string $fieldHandle 372 | * @param bool $isBrand 373 | * 374 | * @return string 375 | */ 376 | protected function pullDataFromField($productVariant, $fieldHandle, $isBrand = false): string 377 | { 378 | $result = ''; 379 | if ($productVariant && $fieldHandle) { 380 | $srcField = $productVariant[$fieldHandle] ?? $productVariant->product[$fieldHandle] ?? null; 381 | // Handle eager loaded elements 382 | if (is_array($srcField)) { 383 | return $this->getDataFromElements($isBrand, $srcField); 384 | } 385 | // If the source field isn't an object, return nothing 386 | if (!is_object($srcField)) { 387 | return $result; 388 | } 389 | switch (get_class($srcField)) { 390 | case TagQuery::class: 391 | break; 392 | case CategoryQuery::class: 393 | case EntryQuery::class: 394 | $result = $this->getDataFromElements($isBrand, $srcField->all()); 395 | break; 396 | 397 | default: 398 | $result = strip_tags($srcField->__toString()); 399 | break; 400 | } 401 | } 402 | 403 | return $result; 404 | } 405 | 406 | /** 407 | * @param bool $isBrand 408 | * @param array $elements 409 | * @return string 410 | */ 411 | protected function getDataFromElements(bool $isBrand, array $elements): string 412 | { 413 | $cats = []; 414 | 415 | if ($isBrand) { 416 | // Because we can only have one brand, we'll get 417 | // the very last category. This means if our 418 | // brand is a sub-category, we'll get the child 419 | // not the parent. 420 | foreach ($elements as $cat) { 421 | $cats = [$cat->title]; 422 | } 423 | } else { 424 | // For every category, show its ancestors 425 | // delimited by a slash. 426 | foreach ($elements as $cat) { 427 | $name = $cat->title; 428 | 429 | while ($cat = $cat->parent) { 430 | $name = $cat->title . '/' . $name; 431 | } 432 | 433 | $cats[] = $name; 434 | } 435 | } 436 | 437 | // Join separate categories with a pipe. 438 | return implode('|', $cats); 439 | } 440 | 441 | /** 442 | * Create an item parameter and set affiliation on it, if any exists. 443 | * 444 | * @return ItemParameter 445 | */ 446 | protected function getNewItemParameter(): ItemParameter 447 | { 448 | $parameter = new ItemParameter(); 449 | $parameter->setAffiliation(InstantAnalytics::$plugin->ga4->getAnalytics()->getAffiliation()); 450 | $parameter->setCurrency(CommercePlugin::getInstance()->getPaymentCurrencies()->getPrimaryPaymentCurrencyIso()); 451 | 452 | return $parameter; 453 | } 454 | } 455 | -------------------------------------------------------------------------------- /src/web/assets/dist/assets/welcome-BP_XNmLM.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @vue/shared v3.5.5 3 | * (c) 2018-present Yuxi (Evan) You and Vue contributors 4 | * @license MIT 5 | **//*! #__NO_SIDE_EFFECTS__ */function ws(e){const t=Object.create(null);for(const s of e.split(","))t[s]=1;return s=>s in t}const V={},Ye=[],ve=()=>{},Di=()=>!1,Vt=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),Es=e=>e.startsWith("onUpdate:"),J=Object.assign,Ss=(e,t)=>{const s=e.indexOf(t);s>-1&&e.splice(s,1)},Hi=Object.prototype.hasOwnProperty,D=(e,t)=>Hi.call(e,t),I=Array.isArray,ct=e=>Ut(e)==="[object Map]",Li=e=>Ut(e)==="[object Set]",P=e=>typeof e=="function",q=e=>typeof e=="string",et=e=>typeof e=="symbol",z=e=>e!==null&&typeof e=="object",In=e=>(z(e)||P(e))&&P(e.then)&&P(e.catch),ji=Object.prototype.toString,Ut=e=>ji.call(e),Ni=e=>Ut(e).slice(8,-1),$i=e=>Ut(e)==="[object Object]",Cs=e=>q(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,ft=ws(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),Wt=e=>{const t=Object.create(null);return s=>t[s]||(t[s]=e(s))},Bi=/-(\w)/g,Ke=Wt(e=>e.replace(Bi,(t,s)=>s?s.toUpperCase():"")),Vi=/\B([A-Z])/g,Ge=Wt(e=>e.replace(Vi,"-$1").toLowerCase()),An=Wt(e=>e.charAt(0).toUpperCase()+e.slice(1)),kt=Wt(e=>e?`on${An(e)}`:""),ze=(e,t)=>!Object.is(e,t),es=(e,...t)=>{for(let s=0;s{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:n,value:s})},Ui=e=>{const t=parseFloat(e);return isNaN(t)?e:t};let ks;const Rn=()=>ks||(ks=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Ts(e){if(I(e)){const t={};for(let s=0;s{if(s){const n=s.split(Ki);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function Os(e){let t="";if(q(e))t=e;else if(I(e))for(let s=0;s0)return;let e;for(;ut;){let t=ut;for(ut=void 0;t;){const s=t.nextEffect;if(t.nextEffect=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(n){e||(e=n)}t=s}}if(e)throw e}function jn(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Nn(e){let t,s=e.depsTail,n=s;for(;n;){const i=n.prevDep;n.version===-1?(n===s&&(s=i),Is(n),Zi(n)):t=n,n.dep.activeLink=n.prevActiveLink,n.prevActiveLink=void 0,n=i}e.deps=t,e.depsTail=s}function as(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&$n(t.dep.computed)||t.dep.version!==t.version)return!0;return!!e._dirty}function $n(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===gt))return;e.globalVersion=gt;const t=e.dep;if(e.flags|=2,t.version>0&&!e.isSSR&&!as(e)){e.flags&=-3;return}const s=N,n=ae;N=e,ae=!0;try{jn(e);const i=e.fn(e._value);(t.version===0||ze(i,e._value))&&(e._value=i,t.version++)}catch(i){throw t.version++,i}finally{N=s,ae=n,Nn(e),e.flags&=-3}}function Is(e){const{dep:t,prevSub:s,nextSub:n}=e;if(s&&(s.nextSub=n,e.prevSub=void 0),n&&(n.prevSub=s,e.nextSub=void 0),t.subs===e&&(t.subs=s),!t.subs&&t.computed){t.computed.flags&=-5;for(let i=t.computed.deps;i;i=i.nextDep)Is(i)}}function Zi(e){const{prevDep:t,nextDep:s}=e;t&&(t.nextDep=s,e.prevDep=void 0),s&&(s.prevDep=t,e.nextDep=void 0)}let ae=!0;const Bn=[];function He(){Bn.push(ae),ae=!1}function Le(){const e=Bn.pop();ae=e===void 0?!0:e}function en(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const s=N;N=void 0;try{t()}finally{N=s}}}let gt=0;class Qi{constructor(t,s){this.sub=t,this.dep=s,this.version=s.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class Vn{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0}track(t){if(!N||!ae||N===this.computed)return;let s=this.activeLink;if(s===void 0||s.sub!==N)s=this.activeLink=new Qi(N,this),N.deps?(s.prevDep=N.depsTail,N.depsTail.nextDep=s,N.depsTail=s):N.deps=N.depsTail=s,N.flags&4&&Un(s);else if(s.version===-1&&(s.version=this.version,s.nextDep)){const n=s.nextDep;n.prevDep=s.prevDep,s.prevDep&&(s.prevDep.nextDep=n),s.prevDep=N.depsTail,s.nextDep=void 0,N.depsTail.nextDep=s,N.depsTail=s,N.deps===s&&(N.deps=n)}return s}trigger(t){this.version++,gt++,this.notify(t)}notify(t){Ms();try{for(let s=this.subs;s;s=s.prevSub)s.sub.notify()}finally{Ps()}}}function Un(e){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let n=t.deps;n;n=n.nextDep)Un(n)}const s=e.dep.subs;s!==e&&(e.prevSub=s,s&&(s.nextSub=e)),e.dep.subs=e}const hs=new WeakMap,We=Symbol(""),ds=Symbol(""),mt=Symbol("");function X(e,t,s){if(ae&&N){let n=hs.get(e);n||hs.set(e,n=new Map);let i=n.get(s);i||n.set(s,i=new Vn),i.track()}}function Te(e,t,s,n,i,r){const l=hs.get(e);if(!l){gt++;return}const f=u=>{u&&u.trigger()};if(Ms(),t==="clear")l.forEach(f);else{const u=I(e),d=u&&Cs(s);if(u&&s==="length"){const a=Number(n);l.forEach((p,y)=>{(y==="length"||y===mt||!et(y)&&y>=a)&&f(p)})}else switch(s!==void 0&&f(l.get(s)),d&&f(l.get(mt)),t){case"add":u?d&&f(l.get("length")):(f(l.get(We)),ct(e)&&f(l.get(ds)));break;case"delete":u||(f(l.get(We)),ct(e)&&f(l.get(ds)));break;case"set":ct(e)&&f(l.get(We));break}}Ps()}function qe(e){const t=j(e);return t===e?t:(X(t,"iterate",mt),xe(e)?t:t.map(ce))}function As(e){return X(e=j(e),"iterate",mt),e}const ki={__proto__:null,[Symbol.iterator](){return ss(this,Symbol.iterator,ce)},concat(...e){return qe(this).concat(...e.map(t=>I(t)?qe(t):t))},entries(){return ss(this,"entries",e=>(e[1]=ce(e[1]),e))},every(e,t){return we(this,"every",e,t,void 0,arguments)},filter(e,t){return we(this,"filter",e,t,s=>s.map(ce),arguments)},find(e,t){return we(this,"find",e,t,ce,arguments)},findIndex(e,t){return we(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return we(this,"findLast",e,t,ce,arguments)},findLastIndex(e,t){return we(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return we(this,"forEach",e,t,void 0,arguments)},includes(...e){return ns(this,"includes",e)},indexOf(...e){return ns(this,"indexOf",e)},join(e){return qe(this).join(e)},lastIndexOf(...e){return ns(this,"lastIndexOf",e)},map(e,t){return we(this,"map",e,t,void 0,arguments)},pop(){return rt(this,"pop")},push(...e){return rt(this,"push",e)},reduce(e,...t){return tn(this,"reduce",e,t)},reduceRight(e,...t){return tn(this,"reduceRight",e,t)},shift(){return rt(this,"shift")},some(e,t){return we(this,"some",e,t,void 0,arguments)},splice(...e){return rt(this,"splice",e)},toReversed(){return qe(this).toReversed()},toSorted(e){return qe(this).toSorted(e)},toSpliced(...e){return qe(this).toSpliced(...e)},unshift(...e){return rt(this,"unshift",e)},values(){return ss(this,"values",ce)}};function ss(e,t,s){const n=As(e),i=n[t]();return n!==e&&!xe(e)&&(i._next=i.next,i.next=()=>{const r=i._next();return r.value&&(r.value=s(r.value)),r}),i}const er=Array.prototype;function we(e,t,s,n,i,r){const l=As(e),f=l!==e&&!xe(e),u=l[t];if(u!==er[t]){const p=u.apply(e,r);return f?ce(p):p}let d=s;l!==e&&(f?d=function(p,y){return s.call(this,ce(p),y,e)}:s.length>2&&(d=function(p,y){return s.call(this,p,y,e)}));const a=u.call(l,d,n);return f&&i?i(a):a}function tn(e,t,s,n){const i=As(e);let r=s;return i!==e&&(xe(e)?s.length>3&&(r=function(l,f,u){return s.call(this,l,f,u,e)}):r=function(l,f,u){return s.call(this,l,ce(f),u,e)}),i[t](r,...n)}function ns(e,t,s){const n=j(e);X(n,"iterate",mt);const i=n[t](...s);return(i===-1||i===!1)&&Ls(s[0])?(s[0]=j(s[0]),n[t](...s)):i}function rt(e,t,s=[]){He(),Ms();const n=j(e)[t].apply(e,s);return Ps(),Le(),n}const tr=ws("__proto__,__v_isRef,__isVue"),Wn=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(et));function sr(e){et(e)||(e=String(e));const t=j(this);return X(t,"has",e),t.hasOwnProperty(e)}class Kn{constructor(t=!1,s=!1){this._isReadonly=t,this._isShallow=s}get(t,s,n){const i=this._isReadonly,r=this._isShallow;if(s==="__v_isReactive")return!i;if(s==="__v_isReadonly")return i;if(s==="__v_isShallow")return r;if(s==="__v_raw")return n===(i?r?gr:Jn:r?qn:Gn).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(n)?t:void 0;const l=I(t);if(!i){let u;if(l&&(u=ki[s]))return u;if(s==="hasOwnProperty")return sr}const f=Reflect.get(t,s,se(t)?t:n);return(et(s)?Wn.has(s):tr(s))||(i||X(t,"get",s),r)?f:se(f)?l&&Cs(s)?f:f.value:z(f)?i?Yn(f):Ds(f):f}}class zn extends Kn{constructor(t=!1){super(!1,t)}set(t,s,n,i){let r=t[s];if(!this._isShallow){const u=Qe(r);if(!xe(n)&&!Qe(n)&&(r=j(r),n=j(n)),!I(t)&&se(r)&&!se(n))return u?!1:(r.value=n,!0)}const l=I(t)&&Cs(s)?Number(s)e,Kt=e=>Reflect.getPrototypeOf(e);function Mt(e,t,s=!1,n=!1){e=e.__v_raw;const i=j(e),r=j(t);s||(ze(t,r)&&X(i,"get",t),X(i,"get",r));const{has:l}=Kt(i),f=n?Fs:s?js:ce;if(l.call(i,t))return f(e.get(t));if(l.call(i,r))return f(e.get(r));e!==i&&e.get(t)}function Pt(e,t=!1){const s=this.__v_raw,n=j(s),i=j(e);return t||(ze(e,i)&&X(n,"has",e),X(n,"has",i)),e===i?s.has(e):s.has(e)||s.has(i)}function It(e,t=!1){return e=e.__v_raw,!t&&X(j(e),"iterate",We),Reflect.get(e,"size",e)}function sn(e,t=!1){!t&&!xe(e)&&!Qe(e)&&(e=j(e));const s=j(this);return Kt(s).has.call(s,e)||(s.add(e),Te(s,"add",e,e)),this}function nn(e,t,s=!1){!s&&!xe(t)&&!Qe(t)&&(t=j(t));const n=j(this),{has:i,get:r}=Kt(n);let l=i.call(n,e);l||(e=j(e),l=i.call(n,e));const f=r.call(n,e);return n.set(e,t),l?ze(t,f)&&Te(n,"set",e,t):Te(n,"add",e,t),this}function rn(e){const t=j(this),{has:s,get:n}=Kt(t);let i=s.call(t,e);i||(e=j(e),i=s.call(t,e)),n&&n.call(t,e);const r=t.delete(e);return i&&Te(t,"delete",e,void 0),r}function ln(){const e=j(this),t=e.size!==0,s=e.clear();return t&&Te(e,"clear",void 0,void 0),s}function At(e,t){return function(n,i){const r=this,l=r.__v_raw,f=j(l),u=t?Fs:e?js:ce;return!e&&X(f,"iterate",We),l.forEach((d,a)=>n.call(i,u(d),u(a),r))}}function Ft(e,t,s){return function(...n){const i=this.__v_raw,r=j(i),l=ct(r),f=e==="entries"||e===Symbol.iterator&&l,u=e==="keys"&&l,d=i[e](...n),a=s?Fs:t?js:ce;return!t&&X(r,"iterate",u?ds:We),{next(){const{value:p,done:y}=d.next();return y?{value:p,done:y}:{value:f?[a(p[0]),a(p[1])]:a(p),done:y}},[Symbol.iterator](){return this}}}}function Ie(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function or(){const e={get(r){return Mt(this,r)},get size(){return It(this)},has:Pt,add:sn,set:nn,delete:rn,clear:ln,forEach:At(!1,!1)},t={get(r){return Mt(this,r,!1,!0)},get size(){return It(this)},has:Pt,add(r){return sn.call(this,r,!0)},set(r,l){return nn.call(this,r,l,!0)},delete:rn,clear:ln,forEach:At(!1,!0)},s={get(r){return Mt(this,r,!0)},get size(){return It(this,!0)},has(r){return Pt.call(this,r,!0)},add:Ie("add"),set:Ie("set"),delete:Ie("delete"),clear:Ie("clear"),forEach:At(!0,!1)},n={get(r){return Mt(this,r,!0,!0)},get size(){return It(this,!0)},has(r){return Pt.call(this,r,!0)},add:Ie("add"),set:Ie("set"),delete:Ie("delete"),clear:Ie("clear"),forEach:At(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(r=>{e[r]=Ft(r,!1,!1),s[r]=Ft(r,!0,!1),t[r]=Ft(r,!1,!0),n[r]=Ft(r,!0,!0)}),[e,s,t,n]}const[cr,fr,ur,ar]=or();function Rs(e,t){const s=t?e?ar:ur:e?fr:cr;return(n,i,r)=>i==="__v_isReactive"?!e:i==="__v_isReadonly"?e:i==="__v_raw"?n:Reflect.get(D(s,i)&&i in n?s:n,i,r)}const hr={get:Rs(!1,!1)},dr={get:Rs(!1,!0)},pr={get:Rs(!0,!1)};const Gn=new WeakMap,qn=new WeakMap,Jn=new WeakMap,gr=new WeakMap;function mr(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function _r(e){return e.__v_skip||!Object.isExtensible(e)?0:mr(Ni(e))}function Ds(e){return Qe(e)?e:Hs(e,!1,ir,hr,Gn)}function br(e){return Hs(e,!1,lr,dr,qn)}function Yn(e){return Hs(e,!0,rr,pr,Jn)}function Hs(e,t,s,n,i){if(!z(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const r=i.get(e);if(r)return r;const l=_r(e);if(l===0)return e;const f=new Proxy(e,l===2?n:s);return i.set(e,f),f}function at(e){return Qe(e)?at(e.__v_raw):!!(e&&e.__v_isReactive)}function Qe(e){return!!(e&&e.__v_isReadonly)}function xe(e){return!!(e&&e.__v_isShallow)}function Ls(e){return e?!!e.__v_raw:!1}function j(e){const t=e&&e.__v_raw;return t?j(t):e}function vr(e){return!D(e,"__v_skip")&&Object.isExtensible(e)&&Fn(e,"__v_skip",!0),e}const ce=e=>z(e)?Ds(e):e,js=e=>z(e)?Yn(e):e;function se(e){return e?e.__v_isRef===!0:!1}function xr(e){return se(e)?e.value:e}const yr={get:(e,t,s)=>t==="__v_raw"?e:xr(Reflect.get(e,t,s)),set:(e,t,s,n)=>{const i=e[t];return se(i)&&!se(s)?(i.value=s,!0):Reflect.set(e,t,s,n)}};function Xn(e){return at(e)?e:new Proxy(e,yr)}class wr{constructor(t,s,n){this.fn=t,this.setter=s,this._value=void 0,this.dep=new Vn(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=gt-1,this.effect=this,this.__v_isReadonly=!s,this.isSSR=n}notify(){this.flags|=16,N!==this&&this.dep.notify()}get value(){const t=this.dep.track();return $n(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function Er(e,t,s=!1){let n,i;return P(e)?n=e:(n=e.get,i=e.set),new wr(n,i,s)}const Rt={},jt=new WeakMap;let Ue;function Sr(e,t=!1,s=Ue){if(s){let n=jt.get(s);n||jt.set(s,n=[]),n.push(e)}}function Cr(e,t,s=V){const{immediate:n,deep:i,once:r,scheduler:l,augmentJob:f,call:u}=s,d=T=>i?T:xe(T)||i===!1||i===0?Re(T,1):Re(T);let a,p,y,M,R=!1,F=!1;if(se(e)?(p=()=>e.value,R=xe(e)):at(e)?(p=()=>d(e),R=!0):I(e)?(F=!0,R=e.some(T=>at(T)||xe(T)),p=()=>e.map(T=>{if(se(T))return T.value;if(at(T))return d(T);if(P(T))return u?u(T,2):T()})):P(e)?t?p=u?()=>u(e,2):e:p=()=>{if(y){He();try{y()}finally{Le()}}const T=Ue;Ue=a;try{return u?u(e,3,[M]):e(M)}finally{Ue=T}}:p=ve,t&&i){const T=p,G=i===!0?1/0:i;p=()=>Re(T(),G)}const Z=Xi(),L=()=>{a.stop(),Z&&Ss(Z.effects,a)};if(r)if(t){const T=t;t=(...G)=>{T(...G),L()}}else{const T=p;p=()=>{T(),L()}}let W=F?new Array(e.length).fill(Rt):Rt;const K=T=>{if(!(!(a.flags&1)||!a.dirty&&!T))if(t){const G=a.run();if(i||R||(F?G.some((Me,he)=>ze(Me,W[he])):ze(G,W))){y&&y();const Me=Ue;Ue=a;try{const he=[G,W===Rt?void 0:F&&W[0]===Rt?[]:W,M];u?u(t,3,he):t(...he),W=G}finally{Ue=Me}}}else a.run()};return f&&f(K),a=new Hn(p),a.scheduler=l?()=>l(K,!1):K,M=T=>Sr(T,!1,a),y=a.onStop=()=>{const T=jt.get(a);if(T){if(u)u(T,4);else for(const G of T)G();jt.delete(a)}},t?n?K(!0):W=a.run():l?l(K.bind(null,!0),!0):a.run(),L.pause=a.pause.bind(a),L.resume=a.resume.bind(a),L.stop=L,L}function Re(e,t=1/0,s){if(t<=0||!z(e)||e.__v_skip||(s=s||new Set,s.has(e)))return e;if(s.add(e),t--,se(e))Re(e.value,t,s);else if(I(e))for(let n=0;n{Re(n,t,s)});else if($i(e)){for(const n in e)Re(e[n],t,s);for(const n of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,n)&&Re(e[n],t,s)}return e}/** 10 | * @vue/runtime-core v3.5.5 11 | * (c) 2018-present Yuxi (Evan) You and Vue contributors 12 | * @license MIT 13 | **/function yt(e,t,s,n){try{return n?e(...n):e()}catch(i){zt(i,t,s)}}function ye(e,t,s,n){if(P(e)){const i=yt(e,t,s,n);return i&&In(i)&&i.catch(r=>{zt(r,t,s)}),i}if(I(e)){const i=[];for(let r=0;r>>1,i=ee[n],r=bt(i);r=bt(s)?ee.push(e):ee.splice(Mr(t),0,e),e.flags|=1,Qn()}}function Qn(){!_t&&!ps&&(ps=!0,Ns=Zn.then(ei))}function Pr(e){I(e)?Xe.push(...e):Ae&&e.id===-1?Ae.splice(Je+1,0,e):e.flags&1||(Xe.push(e),e.flags|=1),Qn()}function on(e,t,s=_t?me+1:0){for(;sbt(s)-bt(n));if(Xe.length=0,Ae){Ae.push(...t);return}for(Ae=t,Je=0;Jee.id==null?e.flags&2?-1:1/0:e.id;function ei(e){ps=!1,_t=!0;try{for(me=0;me{n._d&&gn(-1);const r=Nt(t);let l;try{l=e(...i)}finally{Nt(r),n._d&&gn(1)}return l};return n._n=!0,n._c=!0,n._d=!0,n}function Be(e,t,s,n){const i=e.dirs,r=t&&t.dirs;for(let l=0;le.__isTeleport;function Bs(e,t){e.shapeFlag&6&&e.component?(e.transition=t,Bs(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}/*! #__NO_SIDE_EFFECTS__ */function si(e,t){return P(e)?J({name:e.name},t,{setup:e}):e}function ni(e){e.ids=[e.ids[0]+e.ids[2]+++"-",0,0]}function gs(e,t,s,n,i=!1){if(I(e)){e.forEach((R,F)=>gs(R,t&&(I(t)?t[F]:t),s,n,i));return}if(ht(n)&&!i)return;const r=n.shapeFlag&4?Ks(n.component):n.el,l=i?null:r,{i:f,r:u}=e,d=t&&t.r,a=f.refs===V?f.refs={}:f.refs,p=f.setupState,y=j(p),M=p===V?()=>!1:R=>D(y,R);if(d!=null&&d!==u&&(q(d)?(a[d]=null,M(d)&&(p[d]=null)):se(d)&&(d.value=null)),P(u))yt(u,f,12,[l,a]);else{const R=q(u),F=se(u);if(R||F){const Z=()=>{if(e.f){const L=R?M(u)?p[u]:a[u]:u.value;i?I(L)&&Ss(L,r):I(L)?L.includes(r)||L.push(r):R?(a[u]=[r],M(u)&&(p[u]=a[u])):(u.value=[r],e.k&&(a[e.k]=u.value))}else R?(a[u]=l,M(u)&&(p[u]=l)):F&&(u.value=l,e.k&&(a[e.k]=l))};l?(Z.id=-1,le(Z,s)):Z()}}}const ht=e=>!!e.type.__asyncLoader,ii=e=>e.type.__isKeepAlive;function Rr(e,t){ri(e,"a",t)}function Dr(e,t){ri(e,"da",t)}function ri(e,t,s=te){const n=e.__wdc||(e.__wdc=()=>{let i=s;for(;i;){if(i.isDeactivated)return;i=i.parent}return e()});if(Gt(t,n,s),s){let i=s.parent;for(;i&&i.parent;)ii(i.parent.vnode)&&Hr(n,t,s,i),i=i.parent}}function Hr(e,t,s,n){const i=Gt(t,e,n,!0);oi(()=>{Ss(n[t],i)},s)}function Gt(e,t,s=te,n=!1){if(s){const i=s[e]||(s[e]=[]),r=t.__weh||(t.__weh=(...l)=>{He();const f=wt(s),u=ye(t,s,e,l);return f(),Le(),u});return n?i.unshift(r):i.push(r),r}}const Oe=e=>(t,s=te)=>{(!Yt||e==="sp")&&Gt(e,(...n)=>t(...n),s)},Lr=Oe("bm"),li=Oe("m"),jr=Oe("bu"),Nr=Oe("u"),$r=Oe("bum"),oi=Oe("um"),Br=Oe("sp"),Vr=Oe("rtg"),Ur=Oe("rtc");function Wr(e,t=te){Gt("ec",e,t)}const Kr=Symbol.for("v-ndc"),ms=e=>e?Pi(e)?Ks(e):ms(e.parent):null,dt=J(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>ms(e.parent),$root:e=>ms(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>Vs(e),$forceUpdate:e=>e.f||(e.f=()=>{$s(e.update)}),$nextTick:e=>e.n||(e.n=Or.bind(e.proxy)),$watch:e=>hl.bind(e)}),is=(e,t)=>e!==V&&!e.__isScriptSetup&&D(e,t),zr={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:s,setupState:n,data:i,props:r,accessCache:l,type:f,appContext:u}=e;let d;if(t[0]!=="$"){const M=l[t];if(M!==void 0)switch(M){case 1:return n[t];case 2:return i[t];case 4:return s[t];case 3:return r[t]}else{if(is(n,t))return l[t]=1,n[t];if(i!==V&&D(i,t))return l[t]=2,i[t];if((d=e.propsOptions[0])&&D(d,t))return l[t]=3,r[t];if(s!==V&&D(s,t))return l[t]=4,s[t];_s&&(l[t]=0)}}const a=dt[t];let p,y;if(a)return t==="$attrs"&&X(e.attrs,"get",""),a(e);if((p=f.__cssModules)&&(p=p[t]))return p;if(s!==V&&D(s,t))return l[t]=4,s[t];if(y=u.config.globalProperties,D(y,t))return y[t]},set({_:e},t,s){const{data:n,setupState:i,ctx:r}=e;return is(i,t)?(i[t]=s,!0):n!==V&&D(n,t)?(n[t]=s,!0):D(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(r[t]=s,!0)},has({_:{data:e,setupState:t,accessCache:s,ctx:n,appContext:i,propsOptions:r}},l){let f;return!!s[l]||e!==V&&D(e,l)||is(t,l)||(f=r[0])&&D(f,l)||D(n,l)||D(dt,l)||D(i.config.globalProperties,l)},defineProperty(e,t,s){return s.get!=null?e._.accessCache[t]=0:D(s,"value")&&this.set(e,t,s.value,null),Reflect.defineProperty(e,t,s)}};function cn(e){return I(e)?e.reduce((t,s)=>(t[s]=null,t),{}):e}let _s=!0;function Gr(e){const t=Vs(e),s=e.proxy,n=e.ctx;_s=!1,t.beforeCreate&&fn(t.beforeCreate,e,"bc");const{data:i,computed:r,methods:l,watch:f,provide:u,inject:d,created:a,beforeMount:p,mounted:y,beforeUpdate:M,updated:R,activated:F,deactivated:Z,beforeDestroy:L,beforeUnmount:W,destroyed:K,unmounted:T,render:G,renderTracked:Me,renderTriggered:he,errorCaptured:Pe,serverPrefetch:Et,expose:je,inheritAttrs:tt,components:St,directives:Ct,filters:Zt}=t;if(d&&qr(d,n,null),l)for(const U in l){const $=l[U];P($)&&(n[U]=$.bind(s))}if(i){const U=i.call(s,s);z(U)&&(e.data=Ds(U))}if(_s=!0,r)for(const U in r){const $=r[U],Ne=P($)?$.bind(s,s):P($.get)?$.get.bind(s,s):ve,Tt=!P($)&&P($.set)?$.set.bind(s):ve,$e=Ll({get:Ne,set:Tt});Object.defineProperty(n,U,{enumerable:!0,configurable:!0,get:()=>$e.value,set:de=>$e.value=de})}if(f)for(const U in f)ci(f[U],n,s,U);if(u){const U=P(u)?u.call(s):u;Reflect.ownKeys(U).forEach($=>{kr($,U[$])})}a&&fn(a,e,"c");function Q(U,$){I($)?$.forEach(Ne=>U(Ne.bind(s))):$&&U($.bind(s))}if(Q(Lr,p),Q(li,y),Q(jr,M),Q(Nr,R),Q(Rr,F),Q(Dr,Z),Q(Wr,Pe),Q(Ur,Me),Q(Vr,he),Q($r,W),Q(oi,T),Q(Br,Et),I(je))if(je.length){const U=e.exposed||(e.exposed={});je.forEach($=>{Object.defineProperty(U,$,{get:()=>s[$],set:Ne=>s[$]=Ne})})}else e.exposed||(e.exposed={});G&&e.render===ve&&(e.render=G),tt!=null&&(e.inheritAttrs=tt),St&&(e.components=St),Ct&&(e.directives=Ct),Et&&ni(e)}function qr(e,t,s=ve){I(e)&&(e=bs(e));for(const n in e){const i=e[n];let r;z(i)?"default"in i?r=Dt(i.from||n,i.default,!0):r=Dt(i.from||n):r=Dt(i),se(r)?Object.defineProperty(t,n,{enumerable:!0,configurable:!0,get:()=>r.value,set:l=>r.value=l}):t[n]=r}}function fn(e,t,s){ye(I(e)?e.map(n=>n.bind(t.proxy)):e.bind(t.proxy),t,s)}function ci(e,t,s,n){let i=n.includes(".")?wi(s,n):()=>s[n];if(q(e)){const r=t[e];P(r)&&ls(i,r)}else if(P(e))ls(i,e.bind(s));else if(z(e))if(I(e))e.forEach(r=>ci(r,t,s,n));else{const r=P(e.handler)?e.handler.bind(s):t[e.handler];P(r)&&ls(i,r,e)}}function Vs(e){const t=e.type,{mixins:s,extends:n}=t,{mixins:i,optionsCache:r,config:{optionMergeStrategies:l}}=e.appContext,f=r.get(t);let u;return f?u=f:!i.length&&!s&&!n?u=t:(u={},i.length&&i.forEach(d=>$t(u,d,l,!0)),$t(u,t,l)),z(t)&&r.set(t,u),u}function $t(e,t,s,n=!1){const{mixins:i,extends:r}=t;r&&$t(e,r,s,!0),i&&i.forEach(l=>$t(e,l,s,!0));for(const l in t)if(!(n&&l==="expose")){const f=Jr[l]||s&&s[l];e[l]=f?f(e[l],t[l]):t[l]}return e}const Jr={data:un,props:an,emits:an,methods:ot,computed:ot,beforeCreate:k,created:k,beforeMount:k,mounted:k,beforeUpdate:k,updated:k,beforeDestroy:k,beforeUnmount:k,destroyed:k,unmounted:k,activated:k,deactivated:k,errorCaptured:k,serverPrefetch:k,components:ot,directives:ot,watch:Xr,provide:un,inject:Yr};function un(e,t){return t?e?function(){return J(P(e)?e.call(this,this):e,P(t)?t.call(this,this):t)}:t:e}function Yr(e,t){return ot(bs(e),bs(t))}function bs(e){if(I(e)){const t={};for(let s=0;s1)return s&&P(t)?t.call(n&&n.proxy):t}}const ui={},ai=()=>Object.create(ui),hi=e=>Object.getPrototypeOf(e)===ui;function el(e,t,s,n=!1){const i={},r=ai();e.propsDefaults=Object.create(null),di(e,t,i,r);for(const l in e.propsOptions[0])l in i||(i[l]=void 0);s?e.props=n?i:br(i):e.type.props?e.props=i:e.props=r,e.attrs=r}function tl(e,t,s,n){const{props:i,attrs:r,vnode:{patchFlag:l}}=e,f=j(i),[u]=e.propsOptions;let d=!1;if((n||l>0)&&!(l&16)){if(l&8){const a=e.vnode.dynamicProps;for(let p=0;p{u=!0;const[y,M]=pi(p,t,!0);J(l,y),M&&f.push(...M)};!s&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}if(!r&&!u)return z(e)&&n.set(e,Ye),Ye;if(I(r))for(let a=0;ae[0]==="_"||e==="$stable",Us=e=>I(e)?e.map(_e):[_e(e)],nl=(e,t,s)=>{if(t._n)return t;const n=Ir((...i)=>Us(t(...i)),s);return n._c=!1,n},mi=(e,t,s)=>{const n=e._ctx;for(const i in e){if(gi(i))continue;const r=e[i];if(P(r))t[i]=nl(i,r,n);else if(r!=null){const l=Us(r);t[i]=()=>l}}},_i=(e,t)=>{const s=Us(t);e.slots.default=()=>s},bi=(e,t,s)=>{for(const n in t)(s||n!=="_")&&(e[n]=t[n])},il=(e,t,s)=>{const n=e.slots=ai();if(e.vnode.shapeFlag&32){const i=t._;i?(bi(n,t,s),s&&Fn(n,"_",i,!0)):mi(t,n)}else t&&_i(e,t)},rl=(e,t,s)=>{const{vnode:n,slots:i}=e;let r=!0,l=V;if(n.shapeFlag&32){const f=t._;f?s&&f===1?r=!1:bi(i,t,s):(r=!t.$stable,mi(t,i)),l=t}else t&&(_i(e,t),l={default:1});if(r)for(const f in i)!gi(f)&&l[f]==null&&delete i[f]},le=vl;function ll(e){return ol(e)}function ol(e,t){const s=Rn();s.__VUE__=!0;const{insert:n,remove:i,patchProp:r,createElement:l,createText:f,createComment:u,setText:d,setElementText:a,parentNode:p,nextSibling:y,setScopeId:M=ve,insertStaticContent:R}=e,F=(o,c,h,_=null,g=null,m=null,w=void 0,x=null,v=!!c.dynamicChildren)=>{if(o===c)return;o&&!lt(o,c)&&(_=Ot(o),de(o,g,m,!0),o=null),c.patchFlag===-2&&(v=!1,c.dynamicChildren=null);const{type:b,ref:C,shapeFlag:E}=c;switch(b){case Jt:Z(o,c,h,_);break;case vt:L(o,c,h,_);break;case cs:o==null&&W(c,h,_,w);break;case Ce:St(o,c,h,_,g,m,w,x,v);break;default:E&1?G(o,c,h,_,g,m,w,x,v):E&6?Ct(o,c,h,_,g,m,w,x,v):(E&64||E&128)&&b.process(o,c,h,_,g,m,w,x,v,nt)}C!=null&&g&&gs(C,o&&o.ref,m,c||o,!c)},Z=(o,c,h,_)=>{if(o==null)n(c.el=f(c.children),h,_);else{const g=c.el=o.el;c.children!==o.children&&d(g,c.children)}},L=(o,c,h,_)=>{o==null?n(c.el=u(c.children||""),h,_):c.el=o.el},W=(o,c,h,_)=>{[o.el,o.anchor]=R(o.children,c,h,_,o.el,o.anchor)},K=({el:o,anchor:c},h,_)=>{let g;for(;o&&o!==c;)g=y(o),n(o,h,_),o=g;n(c,h,_)},T=({el:o,anchor:c})=>{let h;for(;o&&o!==c;)h=y(o),i(o),o=h;i(c)},G=(o,c,h,_,g,m,w,x,v)=>{c.type==="svg"?w="svg":c.type==="math"&&(w="mathml"),o==null?Me(c,h,_,g,m,w,x,v):Et(o,c,g,m,w,x,v)},Me=(o,c,h,_,g,m,w,x)=>{let v,b;const{props:C,shapeFlag:E,transition:S,dirs:O}=o;if(v=o.el=l(o.type,m,C&&C.is,C),E&8?a(v,o.children):E&16&&Pe(o.children,v,null,_,g,rs(o,m),w,x),O&&Be(o,null,_,"created"),he(v,o,o.scopeId,w,_),C){for(const B in C)B!=="value"&&!ft(B)&&r(v,B,null,C[B],m,_);"value"in C&&r(v,"value",null,C.value,m),(b=C.onVnodeBeforeMount)&&ge(b,_,o)}O&&Be(o,null,_,"beforeMount");const A=cl(g,S);A&&S.beforeEnter(v),n(v,c,h),((b=C&&C.onVnodeMounted)||A||O)&&le(()=>{b&&ge(b,_,o),A&&S.enter(v),O&&Be(o,null,_,"mounted")},g)},he=(o,c,h,_,g)=>{if(h&&M(o,h),_)for(let m=0;m<_.length;m++)M(o,_[m]);if(g){let m=g.subTree;if(c===m||Si(m.type)&&(m.ssContent===c||m.ssFallback===c)){const w=g.vnode;he(o,w,w.scopeId,w.slotScopeIds,g.parent)}}},Pe=(o,c,h,_,g,m,w,x,v=0)=>{for(let b=v;b{const x=c.el=o.el;let{patchFlag:v,dynamicChildren:b,dirs:C}=c;v|=o.patchFlag&16;const E=o.props||V,S=c.props||V;let O;if(h&&Ve(h,!1),(O=S.onVnodeBeforeUpdate)&&ge(O,h,c,o),C&&Be(c,o,h,"beforeUpdate"),h&&Ve(h,!0),(E.innerHTML&&S.innerHTML==null||E.textContent&&S.textContent==null)&&a(x,""),b?je(o.dynamicChildren,b,x,h,_,rs(c,g),m):w||$(o,c,x,null,h,_,rs(c,g),m,!1),v>0){if(v&16)tt(x,E,S,h,g);else if(v&2&&E.class!==S.class&&r(x,"class",null,S.class,g),v&4&&r(x,"style",E.style,S.style,g),v&8){const A=c.dynamicProps;for(let B=0;B{O&&ge(O,h,c,o),C&&Be(c,o,h,"updated")},_)},je=(o,c,h,_,g,m,w)=>{for(let x=0;x{if(c!==h){if(c!==V)for(const m in c)!ft(m)&&!(m in h)&&r(o,m,c[m],null,g,_);for(const m in h){if(ft(m))continue;const w=h[m],x=c[m];w!==x&&m!=="value"&&r(o,m,x,w,g,_)}"value"in h&&r(o,"value",c.value,h.value,g)}},St=(o,c,h,_,g,m,w,x,v)=>{const b=c.el=o?o.el:f(""),C=c.anchor=o?o.anchor:f("");let{patchFlag:E,dynamicChildren:S,slotScopeIds:O}=c;O&&(x=x?x.concat(O):O),o==null?(n(b,h,_),n(C,h,_),Pe(c.children||[],h,C,g,m,w,x,v)):E>0&&E&64&&S&&o.dynamicChildren?(je(o.dynamicChildren,S,h,g,m,w,x),(c.key!=null||g&&c===g.subTree)&&vi(o,c,!0)):$(o,c,h,C,g,m,w,x,v)},Ct=(o,c,h,_,g,m,w,x,v)=>{c.slotScopeIds=x,o==null?c.shapeFlag&512?g.ctx.activate(c,h,_,w,v):Zt(c,h,_,g,m,w,v):Gs(o,c,v)},Zt=(o,c,h,_,g,m,w)=>{const x=o.component=Il(o,_,g);if(ii(o)&&(x.ctx.renderer=nt),Al(x,!1,w),x.asyncDep){if(g&&g.registerDep(x,Q,w),!o.el){const v=x.subTree=De(vt);L(null,v,c,h)}}else Q(x,o,c,h,g,m,w)},Gs=(o,c,h)=>{const _=c.component=o.component;if(_l(o,c,h))if(_.asyncDep&&!_.asyncResolved){U(_,c,h);return}else _.next=c,_.update();else c.el=o.el,_.vnode=c},Q=(o,c,h,_,g,m,w)=>{const x=()=>{if(o.isMounted){let{next:E,bu:S,u:O,parent:A,vnode:B}=o;{const ie=xi(o);if(ie){E&&(E.el=B.el,U(o,E,w)),ie.asyncDep.then(()=>{o.isUnmounted||x()});return}}let H=E,ne;Ve(o,!1),E?(E.el=B.el,U(o,E,w)):E=B,S&&es(S),(ne=E.props&&E.props.onVnodeBeforeUpdate)&&ge(ne,A,E,B),Ve(o,!0);const Y=os(o),ue=o.subTree;o.subTree=Y,F(ue,Y,p(ue.el),Ot(ue),o,g,m),E.el=Y.el,H===null&&bl(o,Y.el),O&&le(O,g),(ne=E.props&&E.props.onVnodeUpdated)&&le(()=>ge(ne,A,E,B),g)}else{let E;const{el:S,props:O}=c,{bm:A,m:B,parent:H,root:ne,type:Y}=o,ue=ht(c);if(Ve(o,!1),A&&es(A),!ue&&(E=O&&O.onVnodeBeforeMount)&&ge(E,H,c),Ve(o,!0),S&&Xs){const ie=()=>{o.subTree=os(o),Xs(S,o.subTree,o,g,null)};ue&&Y.__asyncHydrate?Y.__asyncHydrate(S,o,ie):ie()}else{ne.ce&&ne.ce._injectChildStyle(Y);const ie=o.subTree=os(o);F(null,ie,h,_,o,g,m),c.el=ie.el}if(B&&le(B,g),!ue&&(E=O&&O.onVnodeMounted)){const ie=c;le(()=>ge(E,H,ie),g)}(c.shapeFlag&256||H&&ht(H.vnode)&&H.vnode.shapeFlag&256)&&o.a&&le(o.a,g),o.isMounted=!0,c=h=_=null}};o.scope.on();const v=o.effect=new Hn(x);o.scope.off();const b=o.update=v.run.bind(v),C=o.job=v.runIfDirty.bind(v);C.i=o,C.id=o.uid,v.scheduler=()=>$s(C),Ve(o,!0),b()},U=(o,c,h)=>{c.component=o;const _=o.vnode.props;o.vnode=c,o.next=null,tl(o,c.props,_,h),rl(o,c.children,h),He(),on(o),Le()},$=(o,c,h,_,g,m,w,x,v=!1)=>{const b=o&&o.children,C=o?o.shapeFlag:0,E=c.children,{patchFlag:S,shapeFlag:O}=c;if(S>0){if(S&128){Tt(b,E,h,_,g,m,w,x,v);return}else if(S&256){Ne(b,E,h,_,g,m,w,x,v);return}}O&8?(C&16&&st(b,g,m),E!==b&&a(h,E)):C&16?O&16?Tt(b,E,h,_,g,m,w,x,v):st(b,g,m,!0):(C&8&&a(h,""),O&16&&Pe(E,h,_,g,m,w,x,v))},Ne=(o,c,h,_,g,m,w,x,v)=>{o=o||Ye,c=c||Ye;const b=o.length,C=c.length,E=Math.min(b,C);let S;for(S=0;SC?st(o,g,m,!0,!1,E):Pe(c,h,_,g,m,w,x,v,E)},Tt=(o,c,h,_,g,m,w,x,v)=>{let b=0;const C=c.length;let E=o.length-1,S=C-1;for(;b<=E&&b<=S;){const O=o[b],A=c[b]=v?Fe(c[b]):_e(c[b]);if(lt(O,A))F(O,A,h,null,g,m,w,x,v);else break;b++}for(;b<=E&&b<=S;){const O=o[E],A=c[S]=v?Fe(c[S]):_e(c[S]);if(lt(O,A))F(O,A,h,null,g,m,w,x,v);else break;E--,S--}if(b>E){if(b<=S){const O=S+1,A=OS)for(;b<=E;)de(o[b],g,m,!0),b++;else{const O=b,A=b,B=new Map;for(b=A;b<=S;b++){const re=c[b]=v?Fe(c[b]):_e(c[b]);re.key!=null&&B.set(re.key,b)}let H,ne=0;const Y=S-A+1;let ue=!1,ie=0;const it=new Array(Y);for(b=0;b=Y){de(re,g,m,!0);continue}let pe;if(re.key!=null)pe=B.get(re.key);else for(H=A;H<=S;H++)if(it[H-A]===0&<(re,c[H])){pe=H;break}pe===void 0?de(re,g,m,!0):(it[pe-A]=b+1,pe>=ie?ie=pe:ue=!0,F(re,c[pe],h,null,g,m,w,x,v),ne++)}const Zs=ue?fl(it):Ye;for(H=Zs.length-1,b=Y-1;b>=0;b--){const re=A+b,pe=c[re],Qs=re+1{const{el:m,type:w,transition:x,children:v,shapeFlag:b}=o;if(b&6){$e(o.component.subTree,c,h,_);return}if(b&128){o.suspense.move(c,h,_);return}if(b&64){w.move(o,c,h,nt);return}if(w===Ce){n(m,c,h);for(let E=0;Ex.enter(m),g);else{const{leave:E,delayLeave:S,afterLeave:O}=x,A=()=>n(m,c,h),B=()=>{E(m,()=>{A(),O&&O()})};S?S(m,A,B):B()}else n(m,c,h)},de=(o,c,h,_=!1,g=!1)=>{const{type:m,props:w,ref:x,children:v,dynamicChildren:b,shapeFlag:C,patchFlag:E,dirs:S,cacheIndex:O}=o;if(E===-2&&(g=!1),x!=null&&gs(x,null,h,o,!0),O!=null&&(c.renderCache[O]=void 0),C&256){c.ctx.deactivate(o);return}const A=C&1&&S,B=!ht(o);let H;if(B&&(H=w&&w.onVnodeBeforeUnmount)&&ge(H,c,o),C&6)Ri(o.component,h,_);else{if(C&128){o.suspense.unmount(h,_);return}A&&Be(o,null,c,"beforeUnmount"),C&64?o.type.remove(o,c,h,nt,_):b&&!b.hasOnce&&(m!==Ce||E>0&&E&64)?st(b,c,h,!1,!0):(m===Ce&&E&384||!g&&C&16)&&st(v,c,h),_&&qs(o)}(B&&(H=w&&w.onVnodeUnmounted)||A)&&le(()=>{H&&ge(H,c,o),A&&Be(o,null,c,"unmounted")},h)},qs=o=>{const{type:c,el:h,anchor:_,transition:g}=o;if(c===Ce){Fi(h,_);return}if(c===cs){T(o);return}const m=()=>{i(h),g&&!g.persisted&&g.afterLeave&&g.afterLeave()};if(o.shapeFlag&1&&g&&!g.persisted){const{leave:w,delayLeave:x}=g,v=()=>w(h,m);x?x(o.el,m,v):v()}else m()},Fi=(o,c)=>{let h;for(;o!==c;)h=y(o),i(o),o=h;i(c)},Ri=(o,c,h)=>{const{bum:_,scope:g,job:m,subTree:w,um:x,m:v,a:b}=o;dn(v),dn(b),_&&es(_),g.stop(),m&&(m.flags|=8,de(w,o,c,h)),x&&le(x,c),le(()=>{o.isUnmounted=!0},c),c&&c.pendingBranch&&!c.isUnmounted&&o.asyncDep&&!o.asyncResolved&&o.suspenseId===c.pendingId&&(c.deps--,c.deps===0&&c.resolve())},st=(o,c,h,_=!1,g=!1,m=0)=>{for(let w=m;w{if(o.shapeFlag&6)return Ot(o.component.subTree);if(o.shapeFlag&128)return o.suspense.next();const c=y(o.anchor||o.el),h=c&&c[Ar];return h?y(h):c};let Qt=!1;const Js=(o,c,h)=>{o==null?c._vnode&&de(c._vnode,null,null,!0):F(c._vnode||null,o,c,null,null,null,h),c._vnode=o,Qt||(Qt=!0,on(),kn(),Qt=!1)},nt={p:F,um:de,m:$e,r:qs,mt:Zt,mc:Pe,pc:$,pbc:je,n:Ot,o:e};let Ys,Xs;return{render:Js,hydrate:Ys,createApp:Qr(Js,Ys)}}function rs({type:e,props:t},s){return s==="svg"&&e==="foreignObject"||s==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:s}function Ve({effect:e,job:t},s){s?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function cl(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function vi(e,t,s=!1){const n=e.children,i=t.children;if(I(n)&&I(i))for(let r=0;r>1,e[s[f]]0&&(t[n]=s[r-1]),s[r]=n)}}for(r=s.length,l=s[r-1];r-- >0;)s[r]=l,l=t[l];return s}function xi(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:xi(t)}function dn(e){if(e)for(let t=0;tDt(ul);function ls(e,t,s){return yi(e,t,s)}function yi(e,t,s=V){const{immediate:n,deep:i,flush:r,once:l}=s,f=J({},s);let u;if(Yt)if(r==="sync"){const y=al();u=y.__watcherHandles||(y.__watcherHandles=[])}else if(!t||n)f.once=!0;else return{stop:ve,resume:ve,pause:ve};const d=te;f.call=(y,M,R)=>ye(y,d,M,R);let a=!1;r==="post"?f.scheduler=y=>{le(y,d&&d.suspense)}:r!=="sync"&&(a=!0,f.scheduler=(y,M)=>{M?y():$s(y)}),f.augmentJob=y=>{t&&(y.flags|=4),a&&(y.flags|=2,d&&(y.id=d.uid,y.i=d))};const p=Cr(e,t,f);return u&&u.push(p),p}function hl(e,t,s){const n=this.proxy,i=q(e)?e.includes(".")?wi(n,e):()=>n[e]:e.bind(n,n);let r;P(t)?r=t:(r=t.handler,s=t);const l=wt(this),f=yi(i,r.bind(n),s);return l(),f}function wi(e,t){const s=t.split(".");return()=>{let n=e;for(let i=0;it==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${Ke(t)}Modifiers`]||e[`${Ge(t)}Modifiers`];function pl(e,t,...s){if(e.isUnmounted)return;const n=e.vnode.props||V;let i=s;const r=t.startsWith("update:"),l=r&&dl(n,t.slice(7));l&&(l.trim&&(i=s.map(a=>q(a)?a.trim():a)),l.number&&(i=s.map(Ui)));let f,u=n[f=kt(t)]||n[f=kt(Ke(t))];!u&&r&&(u=n[f=kt(Ge(t))]),u&&ye(u,e,6,i);const d=n[f+"Once"];if(d){if(!e.emitted)e.emitted={};else if(e.emitted[f])return;e.emitted[f]=!0,ye(d,e,6,i)}}function Ei(e,t,s=!1){const n=t.emitsCache,i=n.get(e);if(i!==void 0)return i;const r=e.emits;let l={},f=!1;if(!P(e)){const u=d=>{const a=Ei(d,t,!0);a&&(f=!0,J(l,a))};!s&&t.mixins.length&&t.mixins.forEach(u),e.extends&&u(e.extends),e.mixins&&e.mixins.forEach(u)}return!r&&!f?(z(e)&&n.set(e,null),null):(I(r)?r.forEach(u=>l[u]=null):J(l,r),z(e)&&n.set(e,l),l)}function qt(e,t){return!e||!Vt(t)?!1:(t=t.slice(2).replace(/Once$/,""),D(e,t[0].toLowerCase()+t.slice(1))||D(e,Ge(t))||D(e,t))}function os(e){const{type:t,vnode:s,proxy:n,withProxy:i,propsOptions:[r],slots:l,attrs:f,emit:u,render:d,renderCache:a,props:p,data:y,setupState:M,ctx:R,inheritAttrs:F}=e,Z=Nt(e);let L,W;try{if(s.shapeFlag&4){const T=i||n,G=T;L=_e(d.call(G,T,a,p,M,y,R)),W=f}else{const T=t;L=_e(T.length>1?T(p,{attrs:f,slots:l,emit:u}):T(p,null)),W=t.props?f:gl(f)}}catch(T){pt.length=0,zt(T,e,1),L=De(vt)}let K=L;if(W&&F!==!1){const T=Object.keys(W),{shapeFlag:G}=K;T.length&&G&7&&(r&&T.some(Es)&&(W=ml(W,r)),K=ke(K,W,!1,!0))}return s.dirs&&(K=ke(K,null,!1,!0),K.dirs=K.dirs?K.dirs.concat(s.dirs):s.dirs),s.transition&&Bs(K,s.transition),L=K,Nt(Z),L}const gl=e=>{let t;for(const s in e)(s==="class"||s==="style"||Vt(s))&&((t||(t={}))[s]=e[s]);return t},ml=(e,t)=>{const s={};for(const n in e)(!Es(n)||!(n.slice(9)in t))&&(s[n]=e[n]);return s};function _l(e,t,s){const{props:n,children:i,component:r}=e,{props:l,children:f,patchFlag:u}=t,d=r.emitsOptions;if(t.dirs||t.transition)return!0;if(s&&u>=0){if(u&1024)return!0;if(u&16)return n?pn(n,l,d):!!l;if(u&8){const a=t.dynamicProps;for(let p=0;pe.__isSuspense;function vl(e,t){t&&t.pendingBranch?I(e)?t.effects.push(...e):t.effects.push(e):Pr(e)}const Ce=Symbol.for("v-fgt"),Jt=Symbol.for("v-txt"),vt=Symbol.for("v-cmt"),cs=Symbol.for("v-stc"),pt=[];let fe=null;function Ci(e=!1){pt.push(fe=e?null:[])}function xl(){pt.pop(),fe=pt[pt.length-1]||null}let xt=1;function gn(e){xt+=e,e<0&&fe&&(fe.hasOnce=!0)}function Ti(e){return e.dynamicChildren=xt>0?fe||Ye:null,xl(),xt>0&&fe&&fe.push(e),e}function yl(e,t,s,n,i,r){return Ti(Mi(e,t,s,n,i,r,!0))}function wl(e,t,s,n,i){return Ti(De(e,t,s,n,i,!0))}function El(e){return e?e.__v_isVNode===!0:!1}function lt(e,t){return e.type===t.type&&e.key===t.key}const Oi=({key:e})=>e??null,Ht=({ref:e,ref_key:t,ref_for:s})=>(typeof e=="number"&&(e=""+e),e!=null?q(e)||se(e)||P(e)?{i:be,r:e,k:t,f:!!s}:e:null);function Mi(e,t=null,s=null,n=0,i=null,r=e===Ce?0:1,l=!1,f=!1){const u={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Oi(t),ref:t&&Ht(t),scopeId:ti,slotScopeIds:null,children:s,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:r,patchFlag:n,dynamicProps:i,dynamicChildren:null,appContext:null,ctx:be};return f?(Ws(u,s),r&128&&e.normalize(u)):s&&(u.shapeFlag|=q(s)?8:16),xt>0&&!l&&fe&&(u.patchFlag>0||r&6)&&u.patchFlag!==32&&fe.push(u),u}const De=Sl;function Sl(e,t=null,s=null,n=0,i=null,r=!1){if((!e||e===Kr)&&(e=vt),El(e)){const f=ke(e,t,!0);return s&&Ws(f,s),xt>0&&!r&&fe&&(f.shapeFlag&6?fe[fe.indexOf(e)]=f:fe.push(f)),f.patchFlag=-2,f}if(Hl(e)&&(e=e.__vccOpts),t){t=Cl(t);let{class:f,style:u}=t;f&&!q(f)&&(t.class=Os(f)),z(u)&&(Ls(u)&&!I(u)&&(u=J({},u)),t.style=Ts(u))}const l=q(e)?1:Si(e)?128:Fr(e)?64:z(e)?4:P(e)?2:0;return Mi(e,t,s,n,i,l,r,!0)}function Cl(e){return e?Ls(e)||hi(e)?J({},e):e:null}function ke(e,t,s=!1,n=!1){const{props:i,ref:r,patchFlag:l,children:f,transition:u}=e,d=t?Ol(i||{},t):i,a={__v_isVNode:!0,__v_skip:!0,type:e.type,props:d,key:d&&Oi(d),ref:t&&t.ref?s&&r?I(r)?r.concat(Ht(t)):[r,Ht(t)]:Ht(t):r,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:f,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==Ce?l===-1?16:l|16:l,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:u,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&ke(e.ssContent),ssFallback:e.ssFallback&&ke(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return u&&n&&Bs(a,u.clone(a)),a}function Tl(e=" ",t=0){return De(Jt,null,e,t)}function _e(e){return e==null||typeof e=="boolean"?De(vt):I(e)?De(Ce,null,e.slice()):typeof e=="object"?Fe(e):De(Jt,null,String(e))}function Fe(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:ke(e)}function Ws(e,t){let s=0;const{shapeFlag:n}=e;if(t==null)t=null;else if(I(t))s=16;else if(typeof t=="object")if(n&65){const i=t.default;i&&(i._c&&(i._d=!1),Ws(e,i()),i._c&&(i._d=!0));return}else{s=32;const i=t._;!i&&!hi(t)?t._ctx=be:i===3&&be&&(be.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else P(t)?(t={default:t,_ctx:be},s=32):(t=String(t),n&64?(s=16,t=[Tl(t)]):s=8);e.children=t,e.shapeFlag|=s}function Ol(...e){const t={};for(let s=0;s{let i;return(i=e[s])||(i=e[s]=[]),i.push(n),r=>{i.length>1?i.forEach(l=>l(r)):i[0](r)}};Bt=t("__VUE_INSTANCE_SETTERS__",s=>te=s),xs=t("__VUE_SSR_SETTERS__",s=>Yt=s)}const wt=e=>{const t=te;return Bt(e),e.scope.on(),()=>{e.scope.off(),Bt(t)}},mn=()=>{te&&te.scope.off(),Bt(null)};function Pi(e){return e.vnode.shapeFlag&4}let Yt=!1;function Al(e,t=!1,s=!1){t&&xs(t);const{props:n,children:i}=e.vnode,r=Pi(e);el(e,n,r,t),il(e,i,s);const l=r?Fl(e,t):void 0;return t&&xs(!1),l}function Fl(e,t){const s=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,zr);const{setup:n}=s;if(n){const i=e.setupContext=n.length>1?Dl(e):null,r=wt(e);He();const l=yt(n,e,0,[e.props,i]);if(Le(),r(),In(l)){if(ht(e)||ni(e),l.then(mn,mn),t)return l.then(f=>{_n(e,f,t)}).catch(f=>{zt(f,e,0)});e.asyncDep=l}else _n(e,l,t)}else Ii(e,t)}function _n(e,t,s){P(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:z(t)&&(e.setupState=Xn(t)),Ii(e,s)}let bn;function Ii(e,t,s){const n=e.type;if(!e.render){if(!t&&bn&&!n.render){const i=n.template||Vs(e).template;if(i){const{isCustomElement:r,compilerOptions:l}=e.appContext.config,{delimiters:f,compilerOptions:u}=n,d=J(J({isCustomElement:r,delimiters:f},l),u);n.render=bn(i,d)}}e.render=n.render||ve}{const i=wt(e);He();try{Gr(e)}finally{Le(),i()}}}const Rl={get(e,t){return X(e,"get",""),e[t]}};function Dl(e){const t=s=>{e.exposed=s||{}};return{attrs:new Proxy(e.attrs,Rl),slots:e.slots,emit:e.emit,expose:t}}function Ks(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Xn(vr(e.exposed)),{get(t,s){if(s in t)return t[s];if(s in dt)return dt[s](e)},has(t,s){return s in t||s in dt}})):e.proxy}function Hl(e){return P(e)&&"__vccOpts"in e}const Ll=(e,t)=>Er(e,t,Yt),jl="3.5.5";/** 14 | * @vue/runtime-dom v3.5.5 15 | * (c) 2018-present Yuxi (Evan) You and Vue contributors 16 | * @license MIT 17 | **/let ys;const vn=typeof window<"u"&&window.trustedTypes;if(vn)try{ys=vn.createPolicy("vue",{createHTML:e=>e})}catch{}const Ai=ys?e=>ys.createHTML(e):e=>e,Nl="http://www.w3.org/2000/svg",$l="http://www.w3.org/1998/Math/MathML",Se=typeof document<"u"?document:null,xn=Se&&Se.createElement("template"),Bl={insert:(e,t,s)=>{t.insertBefore(e,s||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,s,n)=>{const i=t==="svg"?Se.createElementNS(Nl,e):t==="mathml"?Se.createElementNS($l,e):s?Se.createElement(e,{is:s}):Se.createElement(e);return e==="select"&&n&&n.multiple!=null&&i.setAttribute("multiple",n.multiple),i},createText:e=>Se.createTextNode(e),createComment:e=>Se.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Se.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,s,n,i,r){const l=s?s.previousSibling:t.lastChild;if(i&&(i===r||i.nextSibling))for(;t.insertBefore(i.cloneNode(!0),s),!(i===r||!(i=i.nextSibling)););else{xn.innerHTML=Ai(n==="svg"?`${e}`:n==="mathml"?`${e}`:e);const f=xn.content;if(n==="svg"||n==="mathml"){const u=f.firstChild;for(;u.firstChild;)f.appendChild(u.firstChild);f.removeChild(u)}t.insertBefore(f,s)}return[l?l.nextSibling:t.firstChild,s?s.previousSibling:t.lastChild]}},Vl=Symbol("_vtc");function Ul(e,t,s){const n=e[Vl];n&&(t=(t?[t,...n]:[...n]).join(" ")),t==null?e.removeAttribute("class"):s?e.setAttribute("class",t):e.className=t}const yn=Symbol("_vod"),Wl=Symbol("_vsh"),Kl=Symbol(""),zl=/(^|;)\s*display\s*:/;function Gl(e,t,s){const n=e.style,i=q(s);let r=!1;if(s&&!i){if(t)if(q(t))for(const l of t.split(";")){const f=l.slice(0,l.indexOf(":")).trim();s[f]==null&&Lt(n,f,"")}else for(const l in t)s[l]==null&&Lt(n,l,"");for(const l in s)l==="display"&&(r=!0),Lt(n,l,s[l])}else if(i){if(t!==s){const l=n[Kl];l&&(s+=";"+l),n.cssText=s,r=zl.test(s)}}else t&&e.removeAttribute("style");yn in e&&(e[yn]=r?n.display:"",e[Wl]&&(n.display="none"))}const wn=/\s*!important$/;function Lt(e,t,s){if(I(s))s.forEach(n=>Lt(e,t,n));else if(s==null&&(s=""),t.startsWith("--"))e.setProperty(t,s);else{const n=ql(e,t);wn.test(s)?e.setProperty(Ge(n),s.replace(wn,""),"important"):e[n]=s}}const En=["Webkit","Moz","ms"],fs={};function ql(e,t){const s=fs[t];if(s)return s;let n=Ke(t);if(n!=="filter"&&n in e)return fs[t]=n;n=An(n);for(let i=0;ius||(kl.then(()=>us=0),us=Date.now());function to(e,t){const s=n=>{if(!n._vts)n._vts=Date.now();else if(n._vts<=s.attached)return;ye(so(n,s.value),t,5,[n])};return s.value=e,s.attached=eo(),s}function so(e,t){if(I(t)){const s=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{s.call(e),e._stopped=!0},t.map(n=>i=>!i._stopped&&n&&n(i))}else return t}const Mn=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,no=(e,t,s,n,i,r)=>{const l=i==="svg";t==="class"?Ul(e,n,l):t==="style"?Gl(e,s,n):Vt(t)?Es(t)||Zl(e,t,s,n,r):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):io(e,t,n,l))?(Jl(e,t,n),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Cn(e,t,n,l,r,t!=="value")):(t==="true-value"?e._trueValue=n:t==="false-value"&&(e._falseValue=n),Cn(e,t,n,l))};function io(e,t,s,n){if(n)return!!(t==="innerHTML"||t==="textContent"||t in e&&Mn(t)&&P(s));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const i=e.tagName;if(i==="IMG"||i==="VIDEO"||i==="CANVAS"||i==="SOURCE")return!1}return Mn(t)&&q(s)?!1:!!(t in e||e._isVueCE&&(/[A-Z]/.test(t)||!q(s)))}const ro=J({patchProp:no},Bl);let Pn;function lo(){return Pn||(Pn=ll(ro))}const oo=(...e)=>{const t=lo().createApp(...e),{mount:s}=t;return t.mount=n=>{const i=fo(n);if(!i)return;const r=t._component;!P(r)&&!r.render&&!r.template&&(r.template=i.innerHTML),i.nodeType===1&&(i.textContent="");const l=s(i,!1,co(i));return i instanceof Element&&(i.removeAttribute("v-cloak"),i.setAttribute("data-v-app","")),l},t};function co(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function fo(e){return q(e)?document.querySelector(e):e}const Ee=(e=1,t=e+1,s=!1)=>{const n=parseFloat(e),i=parseFloat(t),r=Math.random()*(i-n)+n;return s?Math.round(r):r};class Xt{constructor({color:t="blue",size:s=10,dropRate:n=10}={}){this.color=t,this.size=s,this.dropRate=n}setup({canvas:t,wind:s,windPosCoef:n,windSpeedMax:i,count:r}){return this.canvas=t,this.wind=s,this.windPosCoef=n,this.windSpeedMax=i,this.x=Ee(-35,this.canvas.width+35),this.y=Ee(-30,-35),this.d=Ee(150)+10,this.particleSize=Ee(this.size,this.size*2),this.tilt=Ee(10),this.tiltAngleIncremental=(Ee(0,.08)+.04)*(Ee()<.5?-1:1),this.tiltAngle=0,this.angle=Ee(Math.PI*2),this.count=r+1,this.remove=!1,this}update(){this.tiltAngle+=this.tiltAngleIncremental*(Math.cos(this.wind+(this.d+this.x+this.y)*this.windPosCoef)*.2+1),this.y+=(Math.cos(this.angle+this.d)+parseInt(this.dropRate,10))/2,this.x+=(Math.sin(this.angle)+Math.cos(this.wind+(this.d+this.x+this.y)*this.windPosCoef))*this.windSpeedMax,this.y+=Math.sin(this.wind+(this.d+this.x+this.y)*this.windPosCoef)*this.windSpeedMax,this.tilt=Math.sin(this.tiltAngle-this.count/3)*15}pastBottom(){return this.y>this.canvas.height}draw(){this.canvas.ctx.fillStyle=this.color,this.canvas.ctx.beginPath(),this.canvas.ctx.setTransform(Math.cos(this.tiltAngle),Math.sin(this.tiltAngle),0,1,this.x,this.y)}kill(){this.remove=!0}}class uo extends Xt{draw(){super.draw(),this.canvas.ctx.arc(0,0,this.particleSize/2,0,Math.PI*2,!1),this.canvas.ctx.fill()}}class ao extends Xt{draw(){super.draw(),this.canvas.ctx.fillRect(0,0,this.particleSize,this.particleSize/2)}}class ho extends Xt{draw(){super.draw();const t=(s,n,i,r,l,f)=>{this.canvas.ctx.bezierCurveTo(s*(this.particleSize/200),n*(this.particleSize/200),i*(this.particleSize/200),r*(this.particleSize/200),l*(this.particleSize/200),f*(this.particleSize/200))};this.canvas.ctx.moveTo(37.5/this.particleSize,20/this.particleSize),t(75,37,70,25,50,25),t(20,25,20,62.5,20,62.5),t(20,80,40,102,75,120),t(110,102,130,80,130,62.5),t(130,62.5,130,25,100,25),t(85,25,75,37,75,40),this.canvas.ctx.fill()}}class po extends Xt{constructor(t,s){super(t),this.imgEl=s}draw(){super.draw(),this.canvas.ctx.drawImage(this.imgEl,0,0,this.particleSize,this.particleSize)}}class go{constructor(){this.cachedImages={}}createImageElement(t){const s=document.createElement("img");return s.setAttribute("src",t),s}getImageElement(t){return this.cachedImages[t]||(this.cachedImages[t]=this.createImageElement(t)),this.cachedImages[t]}getRandomParticle(t={}){const s=t.particles||[];return s.length<1?{}:s[Math.floor(Math.random()*s.length)]}getDefaults(t={}){return{type:t.defaultType||"circle",size:t.defaultSize||10,dropRate:t.defaultDropRate||10,colors:t.defaultColors||["DodgerBlue","OliveDrab","Gold","pink","SlateBlue","lightblue","Violet","PaleGreen","SteelBlue","SandyBrown","Chocolate","Crimson"],url:null}}create(t){const s=this.getDefaults(t),n=this.getRandomParticle(t),i=Object.assign(s,n),r=Ee(0,i.colors.length-1,!0);if(i.color=i.colors[r],i.type==="circle")return new uo(i);if(i.type==="rect")return new ao(i);if(i.type==="heart")return new ho(i);if(i.type==="image")return new po(i,this.getImageElement(i.url));throw Error(`Unknown particle type: "${i.type}"`)}}class mo{constructor(t){this.items=[],this.pool=[],this.particleOptions=t,this.particleFactory=new go}update(){const t=[],s=[];this.items.forEach(n=>{n.update(),n.pastBottom()?n.remove||(n.setup(this.particleOptions),t.push(n)):s.push(n)}),this.pool.push(...t),this.items=s}draw(){this.items.forEach(t=>t.draw())}add(){this.pool.length>0?this.items.push(this.pool.pop().setup(this.particleOptions)):this.items.push(this.particleFactory.create(this.particleOptions).setup(this.particleOptions))}refresh(){this.items.forEach(t=>{t.kill()}),this.pool=[]}}class zs{constructor(t){const s="confetti-canvas";if(t&&!(t instanceof HTMLCanvasElement))throw new Error("Element is not a valid HTMLCanvasElement");this.isDefault=!t,this.canvas=t||document.getElementById(s)||zs.createDefaultCanvas(s),this.ctx=this.canvas.getContext("2d")}static createDefaultCanvas(t){const s=document.createElement("canvas");return s.style.display="block",s.style.position="fixed",s.style.pointerEvents="none",s.style.top=0,s.style.width="100vw",s.style.height="100vh",s.id=t,document.querySelector("body").appendChild(s),s}get width(){return this.canvas.width}get height(){return this.canvas.height}clear(){this.ctx.setTransform(1,0,0,1,0,0),this.ctx.clearRect(0,0,this.width,this.height)}updateDimensions(){this.isDefault&&(this.width!==window.innerWidth||this.height!==window.innerHeight)&&(this.canvas.width=window.innerWidth,this.canvas.height=window.innerHeight)}}class _o{constructor(){this.setDefaults()}setDefaults(){this.killed=!1,this.framesSinceDrop=0,this.canvas=null,this.canvasEl=null,this.W=0,this.H=0,this.particleManager=null,this.particlesPerFrame=0,this.wind=0,this.windSpeed=1,this.windSpeedMax=1,this.windChange=.01,this.windPosCoef=.002,this.animationId=null}getParticleOptions(t){const s={canvas:this.canvas,W:this.W,H:this.H,wind:this.wind,windPosCoef:this.windPosCoef,windSpeedMax:this.windSpeedMax,count:0};return Object.assign(s,t),s}createParticles(t={}){const s=this.getParticleOptions(t);this.particleManager=new mo(s)}getCanvasElementFromOptions(t){const{canvasId:s,canvasElement:n}=t;let i=n;if(n&&!(n instanceof HTMLCanvasElement))throw new Error("Invalid options: canvasElement is not a valid HTMLCanvasElement");if(s&&n)throw new Error("Invalid options: canvasId and canvasElement are mutually exclusive");if(s&&!i&&(i=document.getElementById(s)),s&&!(i instanceof HTMLCanvasElement))throw new Error(`Invalid options: element with id "${s}" is not a valid HTMLCanvasElement`);return i}start(t={}){this.remove();const s=this.getCanvasElementFromOptions(t);this.canvas=new zs(s),this.canvasEl=s,this.createParticles(t),this.setGlobalOptions(t),this.animationId=requestAnimationFrame(this.mainLoop.bind(this))}setGlobalOptions(t){this.particlesPerFrame=t.particlesPerFrame||2,this.windSpeedMax=t.windSpeedMax||1}stop(){this.killed=!0,this.particlesPerFrame=0}update(t){const s=this.getCanvasElementFromOptions(t);if(this.canvas&&s!==this.canvasEl){this.start(t);return}this.setGlobalOptions(t),this.particleManager&&(this.particleManager.particleOptions=this.getParticleOptions(t),this.particleManager.refresh())}remove(){this.stop(),this.animationId&&cancelAnimationFrame(this.animationId),this.canvas&&this.canvas.clear(),this.setDefaults()}mainLoop(t){this.canvas.updateDimensions(),this.canvas.clear(),this.windSpeed=Math.sin(t/8e3)*this.windSpeedMax,this.wind=this.particleManager.particleOptions.wind+=this.windChange;let s=this.framesSinceDrop*this.particlesPerFrame;for(;s>=1;)this.particleManager.add(),s-=1,this.framesSinceDrop=0;this.particleManager.update(),this.particleManager.draw(),(!this.killed||this.particleManager.items.length)&&(this.animationId=requestAnimationFrame(this.mainLoop.bind(this))),this.framesSinceDrop+=1}}const bo=si({__name:"ConfettiParty",setup(e){const t={defaultType:"rect",defaultSize:15,defaultColors:["DodgerBlue","OliveDrab","Gold","pink","SlateBlue","lightblue","Violet","PaleGreen","SteelBlue","SandyBrown","Chocolate","Crimson"]},s=new _o;return li(()=>{s.start(t),setTimeout(()=>{s.stop()},5e3)}),(n,i)=>(Ci(),yl("div"))}}),vo=si({__name:"App",setup(e){return(t,s)=>(Ci(),wl(bo))}}),xo=async()=>oo(vo).mount("#app-container");xo().then(()=>{console.log()}); 18 | //# sourceMappingURL=welcome-BP_XNmLM.js.map 19 | --------------------------------------------------------------------------------