├── src ├── web │ └── assets │ │ └── dist │ │ ├── assets │ │ ├── app-c30d56fd.js │ │ ├── app-5b295f2f.css │ │ ├── app-c30d56fd.js.map │ │ ├── welcome-71d12b53.js.gz │ │ ├── welcome-71d12b53.js.map.gz │ │ └── welcome-71d12b53.js │ │ ├── manifest.json │ │ └── img │ │ └── InstantAnalytics-icon.svg ├── templates │ ├── _includes │ │ └── macros.twig │ ├── _layouts │ │ └── instantanalytics-cp.twig │ ├── welcome.twig │ └── settings.twig ├── assetbundles │ └── instantanalytics │ │ ├── InstantAnalyticsAsset.php │ │ └── InstantAnalyticsWelcomeAsset.php ├── controllers │ ├── ManifestController.php │ └── TrackController.php ├── icon.svg ├── translations │ └── en │ │ └── instant-analytics.php ├── services │ ├── ServicesTrait.php │ ├── IA.php │ └── Commerce.php ├── variables │ └── InstantAnalyticsVariable.php ├── config.php ├── twigextensions │ └── InstantAnalyticsTwigExtension.php ├── helpers │ ├── IAnalytics.php │ └── Field.php ├── models │ └── Settings.php └── InstantAnalytics.php ├── composer.json ├── README.md ├── LICENSE.md └── CHANGELOG.md /src/web/assets/dist/assets/app-c30d56fd.js: -------------------------------------------------------------------------------- 1 | 2 | //# sourceMappingURL=app-c30d56fd.js.map 3 | -------------------------------------------------------------------------------- /src/web/assets/dist/assets/app-5b295f2f.css: -------------------------------------------------------------------------------- 1 | .block{display:block}.inline-block{display:inline-block} 2 | -------------------------------------------------------------------------------- /src/web/assets/dist/assets/app-c30d56fd.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"app-c30d56fd.js","sources":[],"sourcesContent":[],"names":[],"mappings":""} -------------------------------------------------------------------------------- /src/web/assets/dist/assets/welcome-71d12b53.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nystudio107/craft-instantanalytics/develop-v4/src/web/assets/dist/assets/welcome-71d12b53.js.gz -------------------------------------------------------------------------------- /src/web/assets/dist/assets/welcome-71d12b53.js.map.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nystudio107/craft-instantanalytics/develop-v4/src/web/assets/dist/assets/welcome-71d12b53.js.map.gz -------------------------------------------------------------------------------- /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/_layouts/instantanalytics-cp.twig: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/cp" %} 2 | 3 | {% block head %} 4 | {{ parent() }} 5 | {% set tagOptions = { 6 | 'depends': [ 7 | 'nystudio107\\instantanalytics\\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.css": { 3 | "file": "assets/app-5b295f2f.css", 4 | "src": "src/js/app.css" 5 | }, 6 | "src/js/app.ts": { 7 | "css": [ 8 | "assets/app-5b295f2f.css" 9 | ], 10 | "file": "assets/app-c30d56fd.js", 11 | "isEntry": true, 12 | "src": "src/js/app.ts" 13 | }, 14 | "src/js/welcome.ts": { 15 | "file": "assets/welcome-71d12b53.js", 16 | "isEntry": true, 17 | "src": "src/js/welcome.ts" 18 | } 19 | } -------------------------------------------------------------------------------- /src/assetbundles/instantanalytics/InstantAnalyticsAsset.php: -------------------------------------------------------------------------------- 1 | sourcePath = '@nystudio107/instantanalytics/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/instantanalytics/web/assets/dist'; 33 | 34 | $this->depends = [ 35 | CpAsset::class, 36 | VueAsset::class, 37 | InstantAnalyticsAsset::class, 38 | ]; 39 | 40 | parent::init(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nystudio107/craft-instantanalytics", 3 | "description": "Instant Analytics brings full Google Analytics support to your Twig templates and automatic Craft Commerce integration with Google Enhanced Ecommerce", 4 | "type": "craft-plugin", 5 | "version": "4.0.3", 6 | "keywords": [ 7 | "craft", 8 | "cms", 9 | "craftcms", 10 | "craft-plugin", 11 | "instant analytics", 12 | "google", 13 | "measurement", 14 | "protocol", 15 | "analytics" 16 | ], 17 | "support": { 18 | "docs": "https://nystudio107.com/docs/instant-analytics/", 19 | "issues": "https://nystudio107.com/plugins/instant-analytics/support", 20 | "source": "https://github.com/nystudio107/craft-instantanalytics" 21 | }, 22 | "license": "proprietary", 23 | "authors": [ 24 | { 25 | "name": "nystudio107", 26 | "homepage": "https://nystudio107.com" 27 | } 28 | ], 29 | "require": { 30 | "craftcms/cms": "^4.0.0", 31 | "nystudio107/craft-plugin-vite": "^4.0.0", 32 | "theiconic/php-ga-measurement-protocol": "^2.5.1", 33 | "jaybizzle/crawler-detect": "^1.2.37" 34 | }, 35 | "config": { 36 | "allow-plugins": { 37 | "craftcms/plugin-installer": true, 38 | "yiisoft/yii2-composer": true 39 | }, 40 | "optimize-autoloader": true, 41 | "sort-packages": true 42 | }, 43 | "autoload": { 44 | "psr-4": { 45 | "nystudio107\\instantanalytics\\": "src/" 46 | } 47 | }, 48 | "extra": { 49 | "class": "nystudio107\\instantanalytics\\InstantAnalytics", 50 | "handle": "instant-analytics", 51 | "name": "Instant Analytics" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/controllers/ManifestController.php: -------------------------------------------------------------------------------- 1 | assetManager->getPublishedUrl( 51 | '@nystudio107/instantanalytics/assetbundles/instantanalytics/dist', 52 | true 53 | ); 54 | $url = "{$baseAssetsUrl}/{$resourceType}/{$fileName}"; 55 | 56 | return $this->redirect($url); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 2 | 3 | # DEPRECATED 4 | 5 | This Craft CMS plugin is no longer supported or maintained, but it is fully functional, and you may continue to use it as you see fit. The license also allows you to fork it and make changes as needed for legacy support reasons. 6 | 7 | Google has deprecated the old Google Analytics APIs. Instead, use [Instant Analytics GA4](https://github.com/nystudio107/craft-instantanalytics-ga4) instead. 8 | 9 | 10 | # Instant Analytics plugin for Craft CMS 4.x 11 | 12 | Instant Analytics brings full Google Analytics support to your Twig templates and automatic Craft Commerce integration with Google Enhanced Ecommerce. 13 | 14 | ![Screenshot](./docs/docs/resources/img/plugin-banner.jpg) 15 | 16 | **Note**: _The license fee for this plugin is $59.00 via the Craft Plugin Store._ 17 | 18 | Related: [Instant Analytics for Craft 2.x](https://github.com/nystudio107/instantanalytics) 19 | 20 | ## Requirements 21 | 22 | This plugin requires Craft CMS 4.0.0 or later. Commerce 4 is required for Google Analytics Enhanced eCommerce support. 23 | 24 | ## Installation 25 | 26 | To install the plugin, follow these instructions. 27 | 28 | 1. Open your terminal and go to your Craft project: 29 | 30 | cd /path/to/project 31 | 32 | 2. Then tell Composer to load the plugin: 33 | 34 | composer require nystudio107/craft-instantanalytics 35 | 36 | 3. Install the plugin via `./craft install/plugin instant-analytics` via the CLI, or in the Control Panel, go to Settings → Plugins and click the “Install” button for Instant Analytics. 37 | 38 | You can also install Instant Analytics via the **Plugin Store** in the Craft Control Panel. 39 | 40 | ## Documentation 41 | 42 | Click here -> [Instant Analytics Documentation](https://nystudio107.com/plugins/instant-analytics/documentation) 43 | 44 | ## Instant Analytics Roadmap 45 | 46 | Some things to do, and ideas for potential features: 47 | 48 | * Support for Commerce 3 alpha 49 | 50 | Brought to you by [nystudio107](http://nystudio107.com) 51 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/controllers/TrackController.php: -------------------------------------------------------------------------------- 1 | ia->pageViewAnalytics($url, $title); 49 | $analytics?->sendPageview(); 50 | $this->redirect($url, 200); 51 | } 52 | 53 | /** 54 | * @param string $url 55 | * @param string $eventCategory 56 | * @param string $eventAction 57 | * @param string $eventLabel 58 | * @param int $eventValue 59 | */ 60 | public function actionTrackEventUrl( 61 | string $url, 62 | string $eventCategory, 63 | string $eventAction, 64 | string $eventLabel, 65 | int $eventValue 66 | ): void 67 | { 68 | $analytics = InstantAnalytics::$plugin->ia->eventAnalytics( 69 | $eventCategory, 70 | $eventAction, 71 | $eventLabel, 72 | $eventValue 73 | ); 74 | // Get the file name 75 | $path = parse_url($url, PHP_URL_PATH); 76 | $analytics?->setDocumentPath($path) 77 | ->sendEvent(); 78 | $this->redirect($url, 200); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/templates/welcome.twig: -------------------------------------------------------------------------------- 1 | {# @var craft \craft\web\twig\variables\CraftVariable #} 2 | {% extends 'instant-analytics/_layouts/instantanalytics-cp.twig' %} 3 | 4 | {% set title = 'Welcome to Instant Analytics!' %} 5 | 6 | {% set docsUrl = "https://github.com/nystudio107/craft-instantanalytics/blob/v1/README.md" %} 7 | {% set linkGetStarted = url('settings/plugins/instant-analytics') %} 8 | 9 | {% do view.registerAssetBundle("nystudio107\\instantanalytics\\assetbundles\\instantanalytics\\InstantAnalyticsWelcomeAsset") %} 10 | {% set baseAssetsUrl = view.getAssetManager().getPublishedUrl('@nystudio107/instantanalytics/web/assets/dist', true) %} 11 | 12 | {% set crumbs = [ 13 | { label: "Instant Analytics", url: url('instant-analytics') }, 14 | { label: "Welcome"|t, url: url('instant-analytics/welcome') }, 15 | ] %} 16 | 17 | {% block head %} 18 | {{ parent() }} 19 | {% set tagOptions = { 20 | 'depends': [ 21 | 'nystudio107\\instantanalytics\\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!

34 |

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

36 |

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

38 | 39 |

For more information, please see the documentation. 40 |

41 |

42 |   43 |

44 |

45 | 46 | 47 | 48 |

49 |
50 |
51 |

52 | Brought to you by nystudio107 53 |

54 |
55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /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(), 39 | // so we can't extract it from the passed in $config 40 | $majorVersion = '4'; 41 | // Dev server container name & port are based on the major version of this plugin 42 | $devPort = 3000 + (int)$majorVersion; 43 | $versionName = 'v' . $majorVersion; 44 | return [ 45 | 'components' => [ 46 | 'ia' => IAService::class, 47 | 'commerce' => CommerceService::class, 48 | // Register the vite service 49 | 'vite' => [ 50 | 'assetClass' => InstantAnalyticsAsset::class, 51 | 'checkDevServer' => true, 52 | 'class' => VitePluginService::class, 53 | 'devServerInternal' => 'http://craft-instantanalytics-' . $versionName . '-buildchain-dev:' . $devPort, 54 | 'devServerPublic' => 'http://localhost:' . $devPort, 55 | 'errorEntry' => 'src/js/app.ts', 56 | 'useDevServer' => true,], 57 | ] 58 | ]; 59 | } 60 | 61 | // Public Methods 62 | // ========================================================================= 63 | 64 | /** 65 | * Returns the ia service 66 | * 67 | * @return IAService The ia service 68 | * @throws InvalidConfigException 69 | */ 70 | public function getIa(): IAService 71 | { 72 | return $this->get('ia'); 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 | ia->pageViewAnalytics($url, $title); 46 | } 47 | 48 | /** 49 | * Get an Event analytics object 50 | * 51 | * @param string $eventCategory 52 | * @param string $eventAction 53 | * @param string $eventLabel 54 | * @param int $eventValue 55 | * 56 | * @return null|IAnalytics 57 | */ 58 | public function eventAnalytics(string $eventCategory = '', string $eventAction = '', string $eventLabel = '', int $eventValue = 0): ?IAnalytics 59 | { 60 | return InstantAnalytics::$plugin->ia->eventAnalytics($eventCategory, $eventAction, $eventLabel, $eventValue); 61 | } 62 | 63 | /** 64 | * Return an Analytics object 65 | * 66 | * @return null|IAnalytics 67 | */ 68 | public function analytics(): ?IAnalytics 69 | { 70 | return InstantAnalytics::$plugin->ia->analytics(); 71 | } 72 | 73 | /** 74 | * Get a PageView tracking URL 75 | * 76 | * @param $url 77 | * @param $title 78 | * 79 | * @return Markup 80 | * @throws Exception 81 | */ 82 | public function pageViewTrackingUrl($url, $title): Markup 83 | { 84 | return Template::raw(InstantAnalytics::$plugin->ia->pageViewTrackingUrl($url, $title)); 85 | } 86 | 87 | /** 88 | * Get an Event tracking URL 89 | * 90 | * @param string $url 91 | * @param string $eventCategory 92 | * @param string $eventAction 93 | * @param string $eventLabel 94 | * @param int $eventValue 95 | * 96 | * @return Markup 97 | * @throws Exception 98 | */ 99 | public function eventTrackingUrl( 100 | string $url, 101 | string $eventCategory = '', 102 | string $eventAction = '', 103 | string $eventLabel = '', 104 | int $eventValue = 0 105 | ): Markup 106 | { 107 | return Template::raw(InstantAnalytics::$plugin->ia->eventTrackingUrl( 108 | $url, 109 | $eventCategory, 110 | $eventAction, 111 | $eventLabel, 112 | $eventValue 113 | )); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/config.php: -------------------------------------------------------------------------------- 1 | '', 30 | 31 | /** 32 | * Should the query string be stripped from the page tracking URL? 33 | */ 34 | 'stripQueryString' => true, 35 | 36 | /** 37 | * Should page views be sent automatically when a page view happens? 38 | */ 39 | 'autoSendPageView' => true, 40 | 41 | /** 42 | * 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. 43 | */ 44 | 'requireGaCookieClientId' => true, 45 | 46 | /** 47 | * Should the GCLID cookie be created if it doesn't exist? 48 | */ 49 | 'createGclidCookie' => true, 50 | 51 | /** 52 | * The field in a Commerce Product Variant that should be used for the category 53 | */ 54 | 'productCategoryField' => '', 55 | 56 | /** 57 | * The field in a Commerce Product Variant that should be used for the brand 58 | */ 59 | 'productBrandField' => '', 60 | 61 | /** 62 | * Whether add to cart events should be automatically sent 63 | */ 64 | 'autoSendAddToCart' => true, 65 | 66 | /** 67 | * Whether remove from cart events should be automatically sent 68 | * 69 | * @var bool 70 | */ 71 | 'autoSendRemoveFromCart' => true, 72 | 73 | /** 74 | * Whether purchase complete events should be automatically sent 75 | */ 76 | 'autoSendPurchaseComplete' => true, 77 | 78 | /** 79 | * Controls whether Instant Analytics will send analytics data. 80 | */ 81 | 'sendAnalyticsData' => true, 82 | 83 | /** 84 | * Controls whether Instant Analytics will send analytics data when `devMode` is on. 85 | */ 86 | 'sendAnalyticsInDevMode' => true, 87 | 88 | /** 89 | * Controls whether we should filter out bot UserGents. 90 | */ 91 | 'filterBotUserAgents' => true, 92 | 93 | /** 94 | * Controls whether we should exclude users logged into an admin account from Analytics tracking. 95 | */ 96 | 'adminExclude' => false, 97 | 98 | /** 99 | * Controls whether analytics that blocked from being sent should be logged to 100 | * storage/logs/web.log 101 | * These are always logged if `devMode` is on 102 | */ 103 | 'logExcludedAnalytics' => true, 104 | 105 | /** 106 | * Contains an array of Craft user group handles to exclude from Analytics tracking. If there's a match 107 | * for any of them, analytics data is not sent. 108 | */ 109 | 'groupExcludes' => array( 110 | 'some_user_group_handle', 111 | ), 112 | 113 | /** 114 | * Contains an array of keys that correspond to $_SERVER[] super-global array keys to test against. 115 | * Each item in the sub-array is tested against the $_SERVER[] super-global key via RegEx; if there's 116 | * a match for any of them, analytics data is not sent. This allows you to filter based on whatever 117 | * information you want. 118 | * Reference: http://php.net/manual/en/reserved.variables.server.php 119 | * RegEx tester: http://regexr.com 120 | */ 121 | 'serverExcludes' => array( 122 | 'REMOTE_ADDR' => array( 123 | '/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/', 124 | ), 125 | ), 126 | ]; 127 | -------------------------------------------------------------------------------- /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\\instantanalytics\\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 Tracking ID:', 27 | instructions: "Enter your Google Analytics Tracking ID here. Only enter the ID, e.g.: UA-XXXXXX-XX, not the entire script code.", 28 | suggestEnvVars: true, 29 | id: 'googleAnalyticsTracking', 30 | name: 'googleAnalyticsTracking', 31 | value: settings['googleAnalyticsTracking'], 32 | }) }} 33 | 34 | {{ forms.lightswitchField({ 35 | label: 'Strip Query String from PageView URLs:', 36 | 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`", 37 | id: 'stripQueryString', 38 | name: 'stripQueryString', 39 | on: settings['stripQueryString']}) }} 40 | 41 | {{ forms.lightswitchField({ 42 | label: 'Auto Send PageViews:', 43 | 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' %}`", 44 | id: 'autoSendPageView', 45 | name: 'autoSendPageView', 46 | on: settings['autoSendPageView']}) }} 47 | 48 | {{ forms.lightswitchField({ 49 | label: 'Require GA Cookie clientId:', 50 | instructions: "If you plan to use Instant Analytics in conjunction with frontend GA JavaScript tracking, this setting should be on, so that Instant Analytics requires a `clientId` from the frontend-set GA cookie before it will send analytics data.", 51 | id: 'requireGaCookieClientId', 52 | name: 'requireGaCookieClientId', 53 | on: settings['requireGaCookieClientId']}) }} 54 | 55 | {{ forms.lightswitchField({ 56 | label: 'Create GCLID Cookie:', 57 | 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.", 58 | id: 'createGclidCookie', 59 | name: 'createGclidCookie', 60 | on: settings['createGclidCookie']}) }} 61 | 62 | {{ forms.lightswitchField({ 63 | label: 'Auto Send "Add To Cart" Events:', 64 | instructions: "If this setting is on, Google Analytics Enhanced Ecommerce events are automatically sent when an item is added to your Craft Commerce cart.", 65 | id: 'autoSendAddToCart', 66 | name: 'autoSendAddToCart', 67 | disabled: (not commerceEnabled), 68 | on: settings['autoSendAddToCart']}) }} 69 | 70 | {{ forms.lightswitchField({ 71 | label: 'Auto Send "Remove From Cart" Events:', 72 | instructions: "If this setting is on, Google Analytics Enhanced Ecommerce events are automatically sent when an item is removed from your Craft Commerce cart.", 73 | id: 'autoSendRemoveFromCart', 74 | name: 'autoSendRemoveFromCart', 75 | disabled: (not commerceEnabled), 76 | on: settings['autoSendRemoveFromCart']}) }} 77 | 78 | {{ forms.lightswitchField({ 79 | label: 'Auto Send "Purchase Complete" Events:', 80 | instructions: "If this setting is on, Google Analytics Enhanced Ecommerce events are automatically sent a purchase is completed.", 81 | id: 'autoSendPurchaseComplete', 82 | name: 'autoSendPurchaseComplete', 83 | disabled: (not commerceEnabled), 84 | on: settings['autoSendPurchaseComplete']}) }} 85 | 86 | {{ forms.selectField({ 87 | label: 'Commerce Product Category Field:', 88 | 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", 89 | id: 'productCategoryField', 90 | name: 'productCategoryField', 91 | options: commerceFields, 92 | disabled: (not commerceEnabled), 93 | value: settings['productCategoryField'], 94 | }) }} 95 | 96 | {{ forms.selectField({ 97 | label: 'Commerce Product Brand Field:', 98 | 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", 99 | id: 'productBrandField', 100 | name: 'productBrandField', 101 | options: commerceFields, 102 | disabled: (not commerceEnabled), 103 | value: settings['productBrandField'], 104 | }) }} 105 | -------------------------------------------------------------------------------- /src/twigextensions/InstantAnalyticsTwigExtension.php: -------------------------------------------------------------------------------- 1 | getView(); 55 | if ($view->getIsRenderingPageTemplate()) { 56 | $request = Craft::$app->getRequest(); 57 | if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) { 58 | // Return our Analytics object as a Twig global 59 | $globals = [ 60 | 'instantAnalytics' => InstantAnalytics::$plugin->ia->getGlobals(InstantAnalytics::$currentTemplate), 61 | ]; 62 | } 63 | } 64 | 65 | return $globals; 66 | } 67 | 68 | /** 69 | * @inheritdoc 70 | */ 71 | public function getFilters(): array 72 | { 73 | return [ 74 | new TwigFilter('pageViewAnalytics', [$this, 'pageViewAnalytics']), 75 | new TwigFilter('eventAnalytics', [$this, 'eventAnalytics']), 76 | new TwigFilter('pageViewTrackingUrl', [$this, 'pageViewTrackingUrl']), 77 | new TwigFilter('eventTrackingUrl', [$this, 'eventTrackingUrl']), 78 | ]; 79 | } 80 | 81 | /** 82 | * @inheritdoc 83 | */ 84 | public function getFunctions(): array 85 | { 86 | return [ 87 | new TwigFunction('pageViewAnalytics', [$this, 'pageViewAnalytics']), 88 | new TwigFunction('eventAnalytics', [$this, 'eventAnalytics']), 89 | new TwigFunction('pageViewTrackingUrl', [$this, 'pageViewTrackingUrl']), 90 | new TwigFunction('eventTrackingUrl', [$this, 'eventTrackingUrl']), 91 | ]; 92 | } 93 | 94 | /** 95 | * Get a PageView analytics object 96 | * 97 | * @param string $url 98 | * @param string $title 99 | * 100 | * @return null|IAnalytics object 101 | */ 102 | public function pageViewAnalytics(string $url = '', string $title = ''): ?IAnalytics 103 | { 104 | return InstantAnalytics::$plugin->ia->pageViewAnalytics($url, $title); 105 | } 106 | 107 | /** 108 | * Get an Event analytics object 109 | * 110 | * @param string $eventCategory 111 | * @param string $eventAction 112 | * @param string $eventLabel 113 | * @param int $eventValue 114 | * 115 | * @return null|IAnalytics 116 | */ 117 | public function eventAnalytics(string $eventCategory = '', string $eventAction = '', string $eventLabel = '', int $eventValue = 0): ?IAnalytics 118 | { 119 | return InstantAnalytics::$plugin->ia->eventAnalytics($eventCategory, $eventAction, $eventLabel, $eventValue); 120 | } 121 | 122 | /** 123 | * Return an Analytics object 124 | * 125 | * @return null|IAnalytics 126 | */ 127 | public function analytics(): ?IAnalytics 128 | { 129 | return InstantAnalytics::$plugin->ia->analytics(); 130 | } 131 | 132 | /** 133 | * Get a PageView tracking URL 134 | * 135 | * @param $url 136 | * @param $title 137 | * 138 | * @return Markup 139 | * @throws Exception 140 | */ 141 | public function pageViewTrackingUrl($url, $title): Markup 142 | { 143 | return Template::raw(InstantAnalytics::$plugin->ia->pageViewTrackingUrl($url, $title)); 144 | } 145 | 146 | /** 147 | * Get an Event tracking URL 148 | * 149 | * @param string $url 150 | * @param string $eventCategory 151 | * @param string $eventAction 152 | * @param string $eventLabel 153 | * @param int $eventValue 154 | * 155 | * @return Markup 156 | * @throws Exception 157 | */ 158 | public function eventTrackingUrl( 159 | string $url, 160 | string $eventCategory = '', 161 | string $eventAction = '', 162 | string $eventLabel = '', 163 | int $eventValue = 0 164 | ): Markup 165 | { 166 | return Template::raw(InstantAnalytics::$plugin->ia->eventTrackingUrl( 167 | $url, 168 | $eventCategory, 169 | $eventAction, 170 | $eventLabel, 171 | $eventValue 172 | )); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/helpers/IAnalytics.php: -------------------------------------------------------------------------------- 1 | shouldSendAnalytics = InstantAnalytics::$settings->sendAnalyticsData; 39 | 40 | parent::__construct($isSsl, $isDisabled, $options); 41 | } 42 | 43 | /** 44 | * Turn an empty value so the twig tags {{ }} can be used 45 | * 46 | * @return string '' 47 | */ 48 | public function __toString() 49 | { 50 | return ''; 51 | } 52 | 53 | /** 54 | * Add a product impression to the Analytics object 55 | * 56 | * @param ?string $productVariant 57 | * @param int $index 58 | * @param string $listName 59 | * @param int $listIndex 60 | */ 61 | public function addCommerceProductImpression( 62 | ?string $productVariant = null, 63 | int $index = 0, 64 | string $listName = 'default', 65 | int $listIndex = 1 66 | ): void 67 | { 68 | 69 | if (InstantAnalytics::$commercePlugin) { 70 | if ($productVariant) { 71 | InstantAnalytics::$plugin->commerce->addCommerceProductImpression( 72 | $this, 73 | $productVariant, 74 | $index, 75 | $listName, 76 | $listIndex 77 | ); 78 | } 79 | } else { 80 | Craft::warning( 81 | Craft::t( 82 | 'instant-analytics', 83 | 'Craft Commerce is not installed' 84 | ), 85 | __METHOD__ 86 | ); 87 | } 88 | } 89 | 90 | /** 91 | * Add a product detail view to the Analytics object 92 | * 93 | * @param null|Product|Variant $productVariant 94 | */ 95 | public function addCommerceProductDetailView(null|Product|Variant $productVariant = null): void 96 | { 97 | if (InstantAnalytics::$commercePlugin) { 98 | if ($productVariant) { 99 | InstantAnalytics::$plugin->commerce->addCommerceProductDetailView($this, $productVariant); 100 | } 101 | } else { 102 | Craft::warning( 103 | Craft::t( 104 | 'instant-analytics', 105 | 'Craft Commerce is not installed' 106 | ), 107 | __METHOD__ 108 | ); 109 | } 110 | } 111 | 112 | /** 113 | * Add a checkout step to the Analytics object 114 | * 115 | * @param $orderModel 116 | * @param int $step 117 | * @param string $option 118 | */ 119 | public function addCommerceCheckoutStep($orderModel = null, $step = 1, $option = ""): void 120 | { 121 | if (InstantAnalytics::$commercePlugin) { 122 | if ($orderModel) { 123 | InstantAnalytics::$plugin->commerce->addCommerceCheckoutStep($this, $orderModel, $step, $option); 124 | } 125 | } else { 126 | Craft::warning( 127 | Craft::t( 128 | 'instant-analytics', 129 | 'Craft Commerce is not installed' 130 | ), 131 | __METHOD__ 132 | ); 133 | } 134 | } 135 | 136 | /** 137 | * Override sendHit() so that we can prevent Analytics data from being sent 138 | * 139 | * @param $methodName 140 | * 141 | * @return AnalyticsResponseInterface|null 142 | */ 143 | protected function sendHit($methodName) 144 | { 145 | $requestIp = $_SERVER['REMOTE_ADDR']; 146 | if ($this->shouldSendAnalytics) { 147 | if ($this->getClientId() !== null || $this->getUserId() !== null) { 148 | try { 149 | Craft::info( 150 | 'Send hit for IAnalytics object: ' . print_r($this, true), 151 | __METHOD__ 152 | ); 153 | 154 | return parent::sendHit($methodName); 155 | } catch (Exception $e) { 156 | if (InstantAnalytics::$settings->logExcludedAnalytics) { 157 | Craft::info( 158 | '*** sendHit(): error sending analytics: ' . $e->getMessage(), 159 | __METHOD__ 160 | ); 161 | } 162 | } 163 | } elseif (InstantAnalytics::$settings->logExcludedAnalytics) { 164 | Craft::info( 165 | '*** sendHit(): analytics not sent for ' . $requestIp . ' because no clientId or userId is set', 166 | __METHOD__ 167 | ); 168 | } 169 | } elseif (InstantAnalytics::$settings->logExcludedAnalytics) { 170 | Craft::info( 171 | '*** sendHit(): analytics not sent for ' . $requestIp, 172 | __METHOD__ 173 | ); 174 | } 175 | 176 | return null; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/models/Settings.php: -------------------------------------------------------------------------------- 1 | [ 157 | '/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/', 158 | ], 159 | ]; 160 | 161 | // Public Methods 162 | // ========================================================================= 163 | 164 | /** 165 | * @return array 166 | */ 167 | public function rules(): array 168 | { 169 | return [ 170 | [ 171 | [ 172 | 'stripQueryString', 173 | 'autoSendPageView', 174 | 'requireGaCookieClientId', 175 | 'createGclidCookie', 176 | 'autoSendAddToCart', 177 | 'autoSendRemoveFromCart', 178 | 'autoSendPurchaseComplete', 179 | 'sendAnalyticsData', 180 | 'sendAnalyticsInDevMode', 181 | 'filterBotUserAgents', 182 | 'adminExclude', 183 | 'logExcludedAnalytics', 184 | ], 185 | 'boolean', 186 | ], 187 | [ 188 | [ 189 | 'googleAnalyticsTracking', 190 | 'productCategoryField', 191 | 'productBrandField', 192 | 'googleAnalyticsTracking', 193 | ], 194 | 'string', 195 | ], 196 | [ 197 | [ 198 | 'groupExcludes', 199 | 'serverExcludes', 200 | ], 201 | ArrayValidator::class, 202 | ], 203 | ]; 204 | } 205 | 206 | /** 207 | * @return array 208 | */ 209 | public function behaviors(): array 210 | { 211 | return [ 212 | 'typecast' => [ 213 | 'class' => AttributeTypecastBehavior::class, 214 | // 'attributeTypes' will be composed automatically according to `rules()` 215 | ], 216 | 'parser' => [ 217 | 'class' => EnvAttributeParserBehavior::class, 218 | 'attributes' => [ 219 | 'googleAnalyticsTracking', 220 | ], 221 | ], 222 | ]; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Instant Analytics Changelog 2 | 3 | ## 4.0.3 - 2023.02.09 4 | ### Changed 5 | * Use dynamic docker container name & port for the `buildchain` 6 | * Refactored the docs buildchain to use a dynamic docker container setup 7 | 8 | ### Fixed 9 | * Fix property types in the `addCommerceProductDetaIlView()` method ([#77](https://github.com/nystudio107/craft-instantanalytics/pull/77)) 10 | 11 | ## 4.0.2 - 2023.01.08 12 | ### Changed 13 | * Updated the docs to use Vitepress `^1.0.0-alpha.29` 14 | * Updated the buildchain to use Vite `^4.0.0` 15 | 16 | ### Fixed 17 | * Fixed an issue where eager loaded categories on Commerce products wouldn't appear in analytics ([#58](https://github.com/nystudio107/craft-instantanalytics/issues/58)) 18 | 19 | ## 4.0.1 - 2022.09.21 20 | ### Fixed 21 | * Fixed an exception that could be thrown if IA's settings in the CP were not filled in ([#73](https://github.com/nystudio107/craft-instantanalytics/issues/73)) 22 | 23 | ## 4.0.0 - 2022.09.16 24 | ### Added 25 | * Initial Craft CMS 4 release 26 | 27 | ### Changed 28 | * Updated how the Instant Analytics are registered, to allow for overriding via plugin config ([#1989](https://github.com/craftcms/cms/issues/1989)) ([#11039](https://github.com/craftcms/cms/pull/11039)) 29 | * Update the buildchain to use Vite `^3.1.0` for building frontend assets 30 | * Move to using `ServicesTrait` and add getter methods for services 31 | 32 | ## 4.0.0-beta.2 - 2022.03.04 33 | 34 | ### Fixed 35 | 36 | * Updated types for Craft CMS `4.0.0-alpha.1` via Rector 37 | 38 | ## 4.0.0-beta.1 - 2022.02.26 39 | 40 | ### Added 41 | 42 | * Initial Craft CMS 4 compatibility 43 | 44 | ## 1.1.15 - 2022.01.27 45 | 46 | ### Fixed 47 | 48 | * Fixed an issue where `craft-plugin-vite` was not included as 49 | required ([#65](https://github.com/nystudio107/craft-instantanalytics/issues/65)) 50 | 51 | ## 1.1.14 - 2022.01.12 52 | 53 | ### Added 54 | 55 | * Add `.gitattributes` & `CODEOWNERS` 56 | * Add linting to build 57 | * Add compression of assets 58 | * Add bundle visualizer 59 | 60 | ## 1.1.13 - 2022.01.05 61 | 62 | ### Changed 63 | 64 | * Switch to Node 16 via `16-alpine` Docker tag by default 65 | * Update to Tailwind CSS `^3.0.0` 66 | * Switched buildchain to Vite & `craft-vite-plugin` 67 | * Refactor to use TypeScript 68 | * Switched documentation system to VitePress 69 | * Use Textlint for the documentation 70 | * Build documentation automatically via GitHub action 71 | 72 | ### Fixed 73 | 74 | * Use `${CURDIR}` instead of `pwd` to be cross-platform compatible with Windows WSL2 75 | 76 | ## 1.1.12 - 2021.04.06 77 | 78 | ### Added 79 | 80 | * Added `make update` to update NPM packages 81 | * Added `make update-clean` to completely remove `node_modules/`, then update NPM packages 82 | 83 | ### Changed 84 | 85 | * More consistent `makefile` build commands 86 | * Use Tailwind CSS `^2.1.0` with JIT 87 | * Move settings from the `composer.json` “extra” to the plugin main class 88 | * Move the manifest service registration to the constructor 89 | 90 | ## 1.1.11 - 2021.03.03 91 | 92 | ### Changed 93 | 94 | * Dockerized the buildchain, using `craft-plugin-manifest` for the webpack HMR bridge 95 | 96 | ## 1.1.10 - 2021.02.15 97 | 98 | ### Changed 99 | 100 | * Verify if purchasable exists before getting its title 101 | * Updated to webpack 5 buildchain 102 | 103 | ## 1.1.9 - 2020.09.02 104 | 105 | ### Added 106 | 107 | * Stash the various `utm` settings in local storage to allow proper attribution from Commerce events 108 | 109 | ### Fixed 110 | 111 | * Set alternative title if item is not a Product (such as a donation) 112 | 113 | ## 1.1.8 - 2020.09.02 114 | 115 | ### Fixed 116 | 117 | * Instant Analytics will no longer attempt to create its own `clientId` by default, instead deferring to 118 | obtaining `clientId` from the GA cookie. Analytics data will not be sent if there is no `clientId`, preventing it from 119 | creating duplicate `usersessions` 120 | 121 | ## 1.1.7 - 2020.04.16 122 | 123 | ### Fixed 124 | 125 | * Fixed Asset Bundle namespace case 126 | 127 | ## 1.1.6 - 2020.04.06 128 | 129 | ### Changed 130 | 131 | * Updated to latest npm dependencies via `npm audit fix` for both the primary app and the docs 132 | * Updated deprecated functions for Commerce 3 133 | 134 | ### Fixed 135 | 136 | * Fixed an issue where an error would be thrown if a brand field didn't exist for a given Product Type 137 | 138 | ## 1.1.5 - 2020.02.05 139 | 140 | ### Fixed 141 | 142 | * Fixed the logic used checking the **Create GCLID Cookie** setting by removing the not `!` 143 | 144 | ## 1.1.4 - 2020.01.15 145 | 146 | ### Added 147 | 148 | * Added **Create GCLID Cookie** setting to control whether ID creates cookies or not 149 | 150 | ## 1.1.3 - 2019.11.25 151 | 152 | ### Added 153 | 154 | * Add currency code to transaction event 155 | 156 | ### Changed 157 | 158 | * Replace use of order number (UID) with the much more human friendly order reference 159 | * Documentation improvements 160 | 161 | ## 1.1.2 - 2019.11.01 162 | 163 | ### Changed 164 | 165 | * Fixed an issue that would cause it to throw an error on the settings page if you didn't have ImageOptimized installed 166 | 167 | ## 1.1.1 - 2019.09.27 168 | 169 | ### Changed 170 | 171 | * Fixed an issue on the Settings page where it would blindly pass in null values to `getLayoutById()` 172 | * If you're using Craft 3.1, Instant Analytics will use 173 | Craft [environmental variables](https://docs.craftcms.com/v3/config/environments.html#control-panel-settings) for 174 | secrets 175 | * Fixed an issue where `get_class()` was passed a non-object 176 | * Updated Twig namespacing to be compliant with deprecated class aliases in 2.7.x 177 | * Updated build system and `package.json` deps as per `npm audit` 178 | 179 | ## 1.1.0 - 2018.11.19 180 | 181 | ### Added 182 | 183 | * Added Craft Commerce 2 support for automatic sending of Google Analytics Enhanced eCommerce events 184 | 185 | ### Changed 186 | 187 | * Retooled the JavaScript build system to be more compatible with edge case server setups 188 | 189 | ## 1.0.11 - 2018.10.05 190 | 191 | ### Changed 192 | 193 | * Updated build process 194 | 195 | ## 1.0.10 - 2018.08.25 196 | 197 | ### Changed 198 | 199 | * Fixed an issue integrating with SEOmatic 200 | 201 | ## 1.0.9 - 2018.08.25 202 | 203 | ### Changed 204 | 205 | * Fixed an issue where the return type-hinting was incorrect 206 | * Handle cases where a `null` IAnalytics object is returned 207 | 208 | ## 1.0.8 - 2018.08.24 209 | 210 | ### Changed 211 | 212 | * Fixed an issue where manually using the `{% hook isSendPageView %}` would throw an error 213 | 214 | ## 1.0.7 - 2018.08.24 215 | 216 | ### Added 217 | 218 | * Added welcome screen after install 219 | * Automatically set the `documentTitle` from the SEOmatic `` tag, if SEOmatic is installed 220 | * Automatically set the `affiliation` from the SEOmatic site name, if SEOmatic is installed 221 | 222 | ### Changed 223 | 224 | * Lots of code cleanup 225 | * Moved to a modern webpack build config for the Control Panel 226 | * Added install confetti 227 | 228 | ## 1.0.6 - 2018.03.22 229 | 230 | ### Added 231 | 232 | * Send only the path, not the full URL to Google Analytics via `eventTrackingUrl()` 233 | * Gutted the Commerce service, pending Craft Commerce 2 234 | 235 | ## 1.0.5 - 2018.02.01 236 | 237 | ### Added 238 | 239 | * Renamed the composer package name to `craft-instantanalytics` 240 | 241 | ## 1.0.4 - 2018.01.10 242 | 243 | ### Changed 244 | 245 | * Set the documentPath for events, too 246 | 247 | ## 1.0.3 - 2018.01.08 248 | 249 | ### Changed 250 | 251 | * Fixed an issue with parsing of the `_ga`_ cookie 252 | 253 | ## 1.0.2 - 2018.01.02 254 | 255 | ### Changed 256 | 257 | * Fixed the `eventTrackingUrl` to work properly 258 | 259 | ## 1.0.1 - 2017.12.06 260 | 261 | ### Changed 262 | 263 | * Updated to require craftcms/cms `^3.0.0-RC1` 264 | * Switched to `Craft::$app->view->registerTwigExtension` to register the Twig extension 265 | 266 | ## 1.0.0 - 2017-10-27 267 | 268 | ### Added 269 | 270 | - Initial release 271 | -------------------------------------------------------------------------------- /src/helpers/Field.php: -------------------------------------------------------------------------------- 1 | <?php 2 | /** 3 | * Instant Analytics plugin for Craft CMS 4 | * 5 | * @author nystudio107 6 | * @copyright Copyright (c) 2017 nystudio107 7 | * @link http://nystudio107.com 8 | * @package InstantAnalytics 9 | * @since 1.0.0 10 | */ 11 | 12 | namespace nystudio107\instantanalytics\helpers; 13 | 14 | use Craft; 15 | use craft\base\Element; 16 | use craft\base\Field as BaseField; 17 | use craft\ckeditor\Field as CKEditorField; 18 | use craft\elements\MatrixBlock; 19 | use craft\elements\User; 20 | use craft\fields\Assets as AssetsField; 21 | use craft\fields\Categories as CategoriesField; 22 | use craft\fields\Matrix as MatrixField; 23 | use craft\fields\PlainText as PlainTextField; 24 | use craft\fields\Tags as TagsField; 25 | use craft\models\FieldLayout; 26 | use craft\models\Volume; 27 | use craft\redactor\Field as RedactorField; 28 | use Exception; 29 | 30 | /** 31 | * @author nystudio107 32 | * @package InstantAnalytics 33 | * @since 1.0.0 34 | */ 35 | class Field 36 | { 37 | // Constants 38 | // ========================================================================= 39 | 40 | public const TEXT_FIELD_CLASS_KEY = 'text'; 41 | public const ASSET_FIELD_CLASS_KEY = 'asset'; 42 | public const BLOCK_FIELD_CLASS_KEY = 'block'; 43 | 44 | protected const FIELD_CLASSES = [ 45 | self::TEXT_FIELD_CLASS_KEY => [ 46 | CKEditorField::class, 47 | PlainTextField::class, 48 | RedactorField::class, 49 | TagsField::class, 50 | CategoriesField::class, 51 | ], 52 | self::ASSET_FIELD_CLASS_KEY => [ 53 | AssetsField::class, 54 | ], 55 | self::BLOCK_FIELD_CLASS_KEY => [ 56 | MatrixField::class, 57 | ], 58 | ]; 59 | 60 | // Static Methods 61 | // ========================================================================= 62 | 63 | /** 64 | * Return all the fields from the $layout that are of the type 65 | * $fieldClassKey 66 | * 67 | * @param string $fieldClassKey 68 | * @param FieldLayout $layout 69 | * @param bool $keysOnly 70 | * 71 | * @return array 72 | */ 73 | public static function fieldsOfTypeFromLayout( 74 | string $fieldClassKey, 75 | FieldLayout $layout, 76 | bool $keysOnly = true 77 | ): array 78 | { 79 | $foundFields = []; 80 | if (!empty(self::FIELD_CLASSES[$fieldClassKey])) { 81 | $fieldClasses = self::FIELD_CLASSES[$fieldClassKey]; 82 | $fields = $layout->getCustomFields(); 83 | /** @var $field BaseField */ 84 | foreach ($fields as $field) { 85 | /** @var array $fieldClasses */ 86 | foreach ($fieldClasses as $fieldClass) { 87 | if ($field instanceof $fieldClass) { 88 | $foundFields[$field->handle] = $field->name; 89 | } 90 | } 91 | } 92 | } 93 | 94 | // Return only the keys if asked 95 | if ($keysOnly) { 96 | $foundFields = array_keys($foundFields); 97 | } 98 | 99 | return $foundFields; 100 | } 101 | 102 | /** 103 | * Return all of the fields in the $element of the type $fieldClassKey 104 | * 105 | * @param Element $element 106 | * @param string $fieldClassKey 107 | * @param bool $keysOnly 108 | * 109 | * @return array 110 | */ 111 | public static function fieldsOfTypeFromElement( 112 | Element $element, 113 | string $fieldClassKey, 114 | bool $keysOnly = true 115 | ): array 116 | { 117 | $foundFields = []; 118 | $layout = $element->getFieldLayout(); 119 | if ($layout !== null) { 120 | $foundFields = self::fieldsOfTypeFromLayout($fieldClassKey, $layout, $keysOnly); 121 | } 122 | 123 | return $foundFields; 124 | } 125 | 126 | /** 127 | * Return all of the fields from Users layout of the type $fieldClassKey 128 | * 129 | * @param string $fieldClassKey 130 | * @param bool $keysOnly 131 | * 132 | * @return array 133 | */ 134 | public static function fieldsOfTypeFromUsers(string $fieldClassKey, bool $keysOnly = true): array 135 | { 136 | $layout = Craft::$app->getFields()->getLayoutByType(User::class); 137 | 138 | return self::fieldsOfTypeFromLayout($fieldClassKey, $layout, $keysOnly); 139 | } 140 | 141 | /** 142 | * Return all the fields from all Asset Volume layouts of the type 143 | * $fieldClassKey 144 | * 145 | * @param string $fieldClassKey 146 | * @param bool $keysOnly 147 | * 148 | * @return array 149 | */ 150 | public static function fieldsOfTypeFromAssetVolumes(string $fieldClassKey, bool $keysOnly = true): array 151 | { 152 | $foundFields = []; 153 | $volumes = Craft::$app->getVolumes()->getAllVolumes(); 154 | foreach ($volumes as $volume) { 155 | /** @var Volume $volume */ 156 | try { 157 | $layout = $volume->getFieldLayout(); 158 | } catch (Exception $e) { 159 | $layout = null; 160 | } 161 | if ($layout) { 162 | /** @noinspection SlowArrayOperationsInLoopInspection */ 163 | $foundFields = array_merge( 164 | $foundFields, 165 | self::fieldsOfTypeFromLayout($fieldClassKey, $layout, $keysOnly) 166 | ); 167 | } 168 | } 169 | 170 | return $foundFields; 171 | } 172 | 173 | /** 174 | * Return all the fields from all Global Set layouts of the type 175 | * $fieldClassKey 176 | * 177 | * @param string $fieldClassKey 178 | * @param bool $keysOnly 179 | * 180 | * @return array 181 | */ 182 | public static function fieldsOfTypeFromGlobals(string $fieldClassKey, bool $keysOnly = true): array 183 | { 184 | $foundFields = []; 185 | $globals = Craft::$app->getGlobals()->getAllSets(); 186 | foreach ($globals as $global) { 187 | $layout = $global->getFieldLayout(); 188 | if ($layout) { 189 | $fields = self::fieldsOfTypeFromLayout($fieldClassKey, $layout, $keysOnly); 190 | // Prefix the keys with the global set name 191 | $prefix = $global->handle; 192 | $fields = array_combine( 193 | array_map(static function ($key) use ($prefix) { 194 | return $prefix . '.' . $key; 195 | }, array_keys($fields)), 196 | $fields 197 | ); 198 | // Merge with any fields we've already found 199 | /** @noinspection SlowArrayOperationsInLoopInspection */ 200 | $foundFields = array_merge( 201 | $foundFields, 202 | $fields 203 | ); 204 | } 205 | } 206 | 207 | return $foundFields; 208 | } 209 | 210 | /** 211 | * Return all the fields in the $matrixBlock of the type $fieldType class 212 | * 213 | * @param MatrixBlock $matrixBlock 214 | * @param string $fieldType 215 | * @param bool $keysOnly 216 | * 217 | * @return array 218 | */ 219 | public static function matrixFieldsOfType(MatrixBlock $matrixBlock, string $fieldType, bool $keysOnly = true): array 220 | { 221 | $foundFields = []; 222 | 223 | try { 224 | $matrixBlockTypeModel = $matrixBlock->getType(); 225 | } catch (Exception $e) { 226 | $matrixBlockTypeModel = null; 227 | } 228 | if ($matrixBlockTypeModel) { 229 | $fields = $matrixBlockTypeModel->getCustomFields(); 230 | /** @var $field BaseField */ 231 | foreach ($fields as $field) { 232 | if ($field instanceof $fieldType) { 233 | $foundFields[$field->handle] = $field->name; 234 | } 235 | } 236 | } 237 | 238 | // Return only the keys if asked 239 | if ($keysOnly) { 240 | $foundFields = array_keys($foundFields); 241 | } 242 | 243 | return $foundFields; 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/InstantAnalytics.php: -------------------------------------------------------------------------------- 1 | <?php 2 | /** 3 | * Instant Analytics plugin for Craft CMS 4 | * 5 | * Instant Analytics brings full Google Analytics support to your Twig templates 6 | * 7 | * @link https://nystudio107.com 8 | * @copyright Copyright (c) 2017 nystudio107 9 | */ 10 | 11 | namespace nystudio107\instantanalytics; 12 | 13 | use Craft; 14 | use craft\base\Model; 15 | use craft\base\Plugin; 16 | use craft\commerce\elements\Order; 17 | use craft\commerce\events\LineItemEvent; 18 | use craft\commerce\Plugin as Commerce; 19 | use craft\events\PluginEvent; 20 | use craft\events\RegisterUrlRulesEvent; 21 | use craft\events\TemplateEvent; 22 | use craft\helpers\UrlHelper; 23 | use craft\services\Plugins; 24 | use craft\web\twig\variables\CraftVariable; 25 | use craft\web\UrlManager; 26 | use craft\web\View; 27 | use Exception; 28 | use nystudio107\instantanalytics\helpers\Field as FieldHelper; 29 | use nystudio107\instantanalytics\helpers\IAnalytics; 30 | use nystudio107\instantanalytics\models\Settings; 31 | use nystudio107\instantanalytics\services\ServicesTrait; 32 | use nystudio107\instantanalytics\twigextensions\InstantAnalyticsTwigExtension; 33 | use nystudio107\instantanalytics\variables\InstantAnalyticsVariable; 34 | use nystudio107\seomatic\Seomatic; 35 | use yii\base\Event; 36 | use function array_merge; 37 | 38 | /** @noinspection MissingPropertyAnnotationsInspection */ 39 | 40 | /** 41 | * @author nystudio107 42 | * @package InstantAnalytics 43 | * @since 1.0.0 44 | */ 45 | class InstantAnalytics extends Plugin 46 | { 47 | // Traits 48 | // ========================================================================= 49 | 50 | use ServicesTrait; 51 | 52 | // Constants 53 | // ========================================================================= 54 | 55 | /** 56 | * @var string 57 | */ 58 | protected const COMMERCE_PLUGIN_HANDLE = 'commerce'; 59 | 60 | /** 61 | * @var string 62 | */ 63 | protected const SEOMATIC_PLUGIN_HANDLE = 'seomatic'; 64 | 65 | // Static Properties 66 | // ========================================================================= 67 | 68 | /** 69 | * @var null|InstantAnalytics 70 | */ 71 | public static ?InstantAnalytics $plugin = null; 72 | 73 | /** 74 | * @var null|Settings 75 | */ 76 | public static ?Settings $settings = null; 77 | 78 | /** 79 | * @var null|Commerce 80 | */ 81 | public static ?Commerce $commercePlugin = null; 82 | 83 | /** 84 | * @var null|Seomatic 85 | */ 86 | public static ?Seomatic $seomaticPlugin = null; 87 | 88 | /** 89 | * @var string 90 | */ 91 | public static string $currentTemplate = ''; 92 | 93 | /** 94 | * @var bool 95 | */ 96 | public static bool $pageViewSent = false; 97 | 98 | // Public Properties 99 | // ========================================================================= 100 | 101 | /** 102 | * @var string 103 | */ 104 | public string $schemaVersion = '1.0.0'; 105 | 106 | /** 107 | * @var bool 108 | */ 109 | public bool $hasCpSection = false; 110 | 111 | /** 112 | * @var bool 113 | */ 114 | public bool $hasCpSettings = true; 115 | 116 | // Public Methods 117 | // ========================================================================= 118 | 119 | /** 120 | * @inheritdoc 121 | */ 122 | public function init(): void 123 | { 124 | parent::init(); 125 | self::$plugin = $this; 126 | self::$settings = $this->getSettings(); 127 | 128 | // Determine if Craft Commerce is installed & enabled 129 | self::$commercePlugin = Craft::$app->getPlugins()->getPlugin(self::COMMERCE_PLUGIN_HANDLE); 130 | // Determine if SEOmatic is installed & enabled 131 | self::$seomaticPlugin = Craft::$app->getPlugins()->getPlugin(self::SEOMATIC_PLUGIN_HANDLE); 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', 140 | '{name} plugin loaded', 141 | ['name' => $this->name] 142 | ), 143 | __METHOD__ 144 | ); 145 | } 146 | 147 | /** 148 | * @inheritdoc 149 | */ 150 | protected function settingsHtml(): ?string 151 | { 152 | $commerceFields = []; 153 | 154 | if (self::$commercePlugin !== null) { 155 | $productTypes = self::$commercePlugin->getProductTypes()->getAllProductTypes(); 156 | 157 | foreach ($productTypes as $productType) { 158 | $productFields = $this->getPullFieldsFromLayoutId($productType->fieldLayoutId); 159 | /** @noinspection SlowArrayOperationsInLoopInspection */ 160 | $commerceFields = array_merge($commerceFields, $productFields); 161 | if ($productType->hasVariants) { 162 | $variantFields = $this->getPullFieldsFromLayoutId($productType->variantFieldLayoutId); 163 | /** @noinspection SlowArrayOperationsInLoopInspection */ 164 | $commerceFields = array_merge($commerceFields, $variantFields); 165 | } 166 | } 167 | } 168 | 169 | // Rend the settings template 170 | try { 171 | return Craft::$app->getView()->renderTemplate( 172 | 'instant-analytics/settings', 173 | [ 174 | 'settings' => $this->getSettings(), 175 | 'commerceFields' => $commerceFields, 176 | ] 177 | ); 178 | } catch (Exception $exception) { 179 | Craft::error($exception->getMessage(), __METHOD__); 180 | } 181 | 182 | return ''; 183 | } 184 | 185 | /** 186 | * Handle the `{% hook iaSendPageView %}` 187 | * 188 | * 189 | */ 190 | public function iaSendPageView(/** @noinspection PhpUnusedParameterInspection */ array &$context): string 191 | { 192 | $this->sendPageView(); 193 | 194 | return ''; 195 | } 196 | 197 | // Protected Methods 198 | // ========================================================================= 199 | 200 | /** 201 | * Add in our Craft components 202 | */ 203 | protected function addComponents(): void 204 | { 205 | $view = Craft::$app->getView(); 206 | // Add in our Twig extensions 207 | $view->registerTwigExtension(new InstantAnalyticsTwigExtension()); 208 | // Install our template hook 209 | $view->hook('iaSendPageView', fn(array $context): string => $this->iaSendPageView($context)); 210 | // Register our variables 211 | Event::on( 212 | CraftVariable::class, 213 | CraftVariable::EVENT_INIT, 214 | function (Event $event): void { 215 | /** @var CraftVariable $variable */ 216 | $variable = $event->sender; 217 | $variable->set('instantAnalytics', [ 218 | 'class' => InstantAnalyticsVariable::class, 219 | 'viteService' => $this->vite, 220 | ]); 221 | } 222 | ); 223 | } 224 | 225 | /** 226 | * Install our event listeners 227 | */ 228 | protected function installEventListeners(): void 229 | { 230 | // Handler: Plugins::EVENT_AFTER_INSTALL_PLUGIN 231 | Event::on( 232 | Plugins::class, 233 | Plugins::EVENT_AFTER_INSTALL_PLUGIN, 234 | function (PluginEvent $event): void { 235 | if ($event->plugin === $this) { 236 | $request = Craft::$app->getRequest(); 237 | if ($request->isCpRequest) { 238 | Craft::$app->getResponse()->redirect(UrlHelper::cpUrl('instant-analytics/welcome'))->send(); 239 | } 240 | } 241 | } 242 | ); 243 | $request = Craft::$app->getRequest(); 244 | // Install only for non-console site requests 245 | if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) { 246 | $this->installSiteEventListeners(); 247 | } 248 | 249 | // Install only for non-console Control Panel requests 250 | if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) { 251 | $this->installCpEventListeners(); 252 | } 253 | } 254 | 255 | /** 256 | * Install site event listeners for site requests only 257 | */ 258 | protected function installSiteEventListeners(): void 259 | { 260 | // Handler: UrlManager::EVENT_REGISTER_SITE_URL_RULES 261 | Event::on( 262 | UrlManager::class, 263 | UrlManager::EVENT_REGISTER_SITE_URL_RULES, 264 | function (RegisterUrlRulesEvent $event): void { 265 | Craft::debug( 266 | 'UrlManager::EVENT_REGISTER_SITE_URL_RULES', 267 | __METHOD__ 268 | ); 269 | // Register our Control Panel routes 270 | $event->rules = array_merge( 271 | $event->rules, 272 | $this->customFrontendRoutes() 273 | ); 274 | } 275 | ); 276 | // Remember the name of the currently rendering template 277 | Event::on( 278 | View::class, 279 | View::EVENT_BEFORE_RENDER_PAGE_TEMPLATE, 280 | static function (TemplateEvent $event): void { 281 | self::$currentTemplate = $event->template; 282 | } 283 | ); 284 | // Remember the name of the currently rendering template 285 | Event::on( 286 | View::class, 287 | View::EVENT_AFTER_RENDER_PAGE_TEMPLATE, 288 | function (TemplateEvent $event): void { 289 | if (self::$settings->autoSendPageView) { 290 | $this->sendPageView(); 291 | } 292 | } 293 | ); 294 | // Commerce-specific hooks 295 | if (self::$commercePlugin !== null) { 296 | Event::on(Order::class, Order::EVENT_AFTER_COMPLETE_ORDER, function (Event $e): void { 297 | $order = $e->sender; 298 | if (self::$settings->autoSendPurchaseComplete) { 299 | $this->commerce->orderComplete($order); 300 | } 301 | }); 302 | 303 | Event::on(Order::class, Order::EVENT_AFTER_ADD_LINE_ITEM, function (LineItemEvent $e): void { 304 | $lineItem = $e->lineItem; 305 | if (self::$settings->autoSendAddToCart) { 306 | $this->commerce->addToCart($lineItem->order, $lineItem); 307 | } 308 | }); 309 | 310 | // Check to make sure Order::EVENT_AFTER_REMOVE_LINE_ITEM is defined 311 | if (defined(Order::class . '::EVENT_AFTER_REMOVE_LINE_ITEM')) { 312 | Event::on(Order::class, Order::EVENT_AFTER_REMOVE_LINE_ITEM, function (LineItemEvent $e): void { 313 | $lineItem = $e->lineItem; 314 | if (self::$settings->autoSendRemoveFromCart) { 315 | $this->commerce->removeFromCart($lineItem->order, $lineItem); 316 | } 317 | }); 318 | } 319 | } 320 | } 321 | 322 | /** 323 | * Install site event listeners for Control Panel requests only 324 | */ 325 | protected function installCpEventListeners(): void 326 | { 327 | } 328 | 329 | /** 330 | * Return the custom frontend routes 331 | * 332 | * @return array<string, string> 333 | */ 334 | protected function customFrontendRoutes(): array 335 | { 336 | return [ 337 | 'instantanalytics/pageViewTrack/<filename:[-\w\.*]+>?' => 338 | 'instant-analytics/track/track-page-view-url', 339 | 'instantanalytics/eventTrack/<filename:[-\w\.*]+>?' => 340 | 'instant-analytics/track/track-event-url', 341 | ]; 342 | } 343 | 344 | /** 345 | * @inheritdoc 346 | */ 347 | protected function createSettingsModel(): ?Model 348 | { 349 | return new Settings(); 350 | } 351 | 352 | // Private Methods 353 | // ========================================================================= 354 | 355 | /** 356 | * Send a page view with the pre-loaded IAnalytics object 357 | */ 358 | private function sendPageView(): void 359 | { 360 | $request = Craft::$app->getRequest(); 361 | if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest() && !self::$pageViewSent) { 362 | self::$pageViewSent = true; 363 | $analytics = self::$plugin->ia->getGlobals(self::$currentTemplate); 364 | // Bail if we have no analytics object 365 | if ($analytics === null) { 366 | return; 367 | } 368 | // If SEOmatic is installed, set the page title from it 369 | $this->setTitleFromSeomatic($analytics); 370 | // Send the page view 371 | $response = $analytics->sendPageview(); 372 | Craft::info( 373 | Craft::t( 374 | 'instant-analytics', 375 | 'pageView sent, response:: {response}', 376 | [ 377 | 'response' => print_r($response, true), 378 | ] 379 | ), 380 | __METHOD__ 381 | ); 382 | } else { 383 | Craft::error( 384 | Craft::t( 385 | 'instant-analytics', 386 | 'Analytics not sent because googleAnalyticsTracking is not set' 387 | ), 388 | __METHOD__ 389 | ); 390 | } 391 | } 392 | 393 | /** 394 | * If SEOmatic is installed, set the page title from it 395 | */ 396 | private function setTitleFromSeomatic(IAnalytics $analytics): void 397 | { 398 | if (self::$seomaticPlugin && Seomatic::$settings->renderEnabled) { 399 | $titleTag = Seomatic::$plugin->title->get('title'); 400 | if ($titleTag !== null) { 401 | $titleArray = $titleTag->renderAttributes(); 402 | if (!empty($titleArray['title'])) { 403 | $analytics->setDocumentTitle($titleArray['title']); 404 | } 405 | } 406 | } 407 | } 408 | 409 | /** 410 | * @param $layoutId 411 | * 412 | * @return mixed[]|array<string, string> 413 | */ 414 | private function getPullFieldsFromLayoutId($layoutId): array 415 | { 416 | $result = ['' => 'none']; 417 | if ($layoutId === null) { 418 | return $result; 419 | } 420 | 421 | $fieldLayout = Craft::$app->getFields()->getLayoutById($layoutId); 422 | if ($fieldLayout) { 423 | $result = FieldHelper::fieldsOfTypeFromLayout(FieldHelper::TEXT_FIELD_CLASS_KEY, $fieldLayout, false); 424 | } 425 | 426 | return $result; 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /src/services/IA.php: -------------------------------------------------------------------------------- 1 | <?php 2 | /** 3 | * Instant Analytics plugin for Craft CMS 4 | * 5 | * Instant Analytics brings full Google Analytics support to your Twig templates 6 | * 7 | * @link https://nystudio107.com 8 | * @copyright Copyright (c) 2017 nystudio107 9 | */ 10 | 11 | namespace nystudio107\instantanalytics\services; 12 | 13 | use Craft; 14 | use craft\base\Component; 15 | use craft\elements\User as UserElement; 16 | use craft\errors\MissingComponentException; 17 | use craft\helpers\UrlHelper; 18 | use Jaybizzle\CrawlerDetect\CrawlerDetect; 19 | use nystudio107\instantanalytics\helpers\IAnalytics; 20 | use nystudio107\instantanalytics\InstantAnalytics; 21 | use nystudio107\seomatic\Seomatic; 22 | use yii\base\Exception; 23 | use function array_slice; 24 | use function is_array; 25 | 26 | /** @noinspection MissingPropertyAnnotationsInspection */ 27 | 28 | /** 29 | * @author nystudio107 30 | * @package InstantAnalytics 31 | * @since 1.0.0 32 | */ 33 | class IA extends Component 34 | { 35 | // Constants 36 | // ========================================================================= 37 | 38 | const DEFAULT_USER_AGENT = "User-Agent:Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13\r\n"; 39 | 40 | // Public Methods 41 | // ========================================================================= 42 | 43 | /** 44 | * @var null|IAnalytics 45 | */ 46 | protected $cachedAnalytics; 47 | 48 | /** 49 | * Get the global variables for our Twig context 50 | * 51 | * @param $title 52 | * 53 | * @return null|IAnalytics 54 | */ 55 | public function getGlobals($title) 56 | { 57 | if ($this->cachedAnalytics) { 58 | $analytics = $this->cachedAnalytics; 59 | } else { 60 | $analytics = $this->pageViewAnalytics('', $title); 61 | $this->cachedAnalytics = $analytics; 62 | } 63 | 64 | return $analytics; 65 | } 66 | 67 | /** 68 | * Get a PageView analytics object 69 | * 70 | * @param string $url 71 | * @param string $title 72 | * 73 | * @return null|IAnalytics 74 | */ 75 | public function pageViewAnalytics($url = '', $title = '') 76 | { 77 | $result = null; 78 | $analytics = $this->analytics(); 79 | if ($analytics) { 80 | $url = $this->documentPathFromUrl($url); 81 | // Prepare the Analytics object, and send the pageview 82 | $analytics->setDocumentPath($url) 83 | ->setDocumentTitle($title); 84 | $result = $analytics; 85 | Craft::info( 86 | Craft::t( 87 | 'instant-analytics', 88 | 'Created sendPageView for: {url} - {title}', 89 | [ 90 | 'url' => $url, 91 | 'title' => $title, 92 | ] 93 | ), 94 | __METHOD__ 95 | ); 96 | } 97 | 98 | return $result; 99 | } 100 | 101 | /** 102 | * Get an Event analytics object 103 | * 104 | * @param string $eventCategory 105 | * @param string $eventAction 106 | * @param string $eventLabel 107 | * @param int $eventValue 108 | * 109 | * @return null|IAnalytics 110 | */ 111 | public function eventAnalytics($eventCategory = '', $eventAction = '', $eventLabel = '', $eventValue = 0) 112 | { 113 | $result = null; 114 | $analytics = $this->analytics(); 115 | if ($analytics) { 116 | $url = $this->documentPathFromUrl(); 117 | $analytics->setDocumentPath($url) 118 | ->setEventCategory($eventCategory) 119 | ->setEventAction($eventAction) 120 | ->setEventLabel($eventLabel) 121 | ->setEventValue((int)$eventValue); 122 | $result = $analytics; 123 | Craft::info( 124 | Craft::t( 125 | 'instant-analytics', 126 | 'Created sendPageView for: {eventCategory} - {eventAction} - {eventLabel} - {eventValue}', 127 | [ 128 | 'eventCategory' => $eventCategory, 129 | 'eventAction' => $eventAction, 130 | 'eventLabel' => $eventLabel, 131 | 'eventValue' => $eventValue, 132 | ] 133 | ), 134 | __METHOD__ 135 | ); 136 | } 137 | 138 | return $result; 139 | } 140 | 141 | /** 142 | * getAnalyticsObject() return an analytics object 143 | * 144 | * @return null|IAnalytics object 145 | */ 146 | public function analytics() 147 | { 148 | $analytics = $this->getAnalyticsObj(); 149 | Craft::info( 150 | Craft::t( 151 | 'instant-analytics', 152 | 'Created generic analytics object' 153 | ), 154 | __METHOD__ 155 | ); 156 | 157 | return $analytics; 158 | } 159 | 160 | /** 161 | * Get a PageView tracking URL 162 | * 163 | * @param $url 164 | * @param $title 165 | * 166 | * @return string 167 | * @throws Exception 168 | */ 169 | public function pageViewTrackingUrl($url, $title): string 170 | { 171 | $urlParams = [ 172 | 'url' => $url, 173 | 'title' => $title, 174 | ]; 175 | $path = parse_url($url, PHP_URL_PATH); 176 | $pathFragments = explode('/', rtrim($path, '/')); 177 | $fileName = end($pathFragments); 178 | $trackingUrl = UrlHelper::siteUrl('instantanalytics/pageViewTrack/' . $fileName, $urlParams); 179 | Craft::info( 180 | Craft::t( 181 | 'instant-analytics', 182 | 'Created pageViewTrackingUrl for: {trackingUrl}', 183 | [ 184 | 'trackingUrl' => $trackingUrl, 185 | ] 186 | ), 187 | __METHOD__ 188 | ); 189 | 190 | return $trackingUrl; 191 | } 192 | 193 | /** 194 | * Get an Event tracking URL 195 | * 196 | * @param $url 197 | * @param string $eventCategory 198 | * @param string $eventAction 199 | * @param string $eventLabel 200 | * @param int $eventValue 201 | * 202 | * @return string 203 | * @throws Exception 204 | */ 205 | public function eventTrackingUrl( 206 | $url, 207 | $eventCategory = '', 208 | $eventAction = '', 209 | $eventLabel = '', 210 | $eventValue = 0 211 | ): string 212 | { 213 | $urlParams = [ 214 | 'url' => $url, 215 | 'eventCategory' => $eventCategory, 216 | 'eventAction' => $eventAction, 217 | 'eventLabel' => $eventLabel, 218 | 'eventValue' => $eventValue, 219 | ]; 220 | $fileName = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_BASENAME); 221 | $trackingUrl = UrlHelper::siteUrl('instantanalytics/eventTrack/' . $fileName, $urlParams); 222 | Craft::info( 223 | Craft::t( 224 | 'instant-analytics', 225 | 'Created eventTrackingUrl for: {trackingUrl}', 226 | [ 227 | 'trackingUrl' => $trackingUrl, 228 | ] 229 | ), 230 | __METHOD__ 231 | ); 232 | 233 | return $trackingUrl; 234 | } 235 | 236 | /** 237 | * _shouldSendAnalytics determines whether we should be sending Google 238 | * Analytics data 239 | * 240 | * @return bool 241 | */ 242 | public function shouldSendAnalytics(): bool 243 | { 244 | $result = true; 245 | 246 | $request = Craft::$app->getRequest(); 247 | 248 | if (!InstantAnalytics::$settings->sendAnalyticsData) { 249 | $this->logExclusion('sendAnalyticsData'); 250 | 251 | return false; 252 | } 253 | 254 | if (!InstantAnalytics::$settings->sendAnalyticsInDevMode && Craft::$app->getConfig()->getGeneral()->devMode) { 255 | $this->logExclusion('sendAnalyticsInDevMode'); 256 | 257 | return false; 258 | } 259 | 260 | if ($request->getIsConsoleRequest()) { 261 | $this->logExclusion('Craft::$app->getRequest()->getIsConsoleRequest()'); 262 | 263 | return false; 264 | } 265 | 266 | if ($request->getIsCpRequest()) { 267 | $this->logExclusion('Craft::$app->getRequest()->getIsCpRequest()'); 268 | 269 | return false; 270 | } 271 | 272 | if ($request->getIsLivePreview()) { 273 | $this->logExclusion('Craft::$app->getRequest()->getIsLivePreview()'); 274 | 275 | return false; 276 | } 277 | 278 | // Check the $_SERVER[] super-global exclusions 279 | if (InstantAnalytics::$settings->serverExcludes !== null 280 | && is_array(InstantAnalytics::$settings->serverExcludes)) { 281 | foreach (InstantAnalytics::$settings->serverExcludes as $match => $matchArray) { 282 | if (isset($_SERVER[$match])) { 283 | foreach ($matchArray as $matchItem) { 284 | if (preg_match($matchItem, $_SERVER[$match])) { 285 | $this->logExclusion('serverExcludes'); 286 | 287 | return false; 288 | } 289 | } 290 | } 291 | } 292 | } 293 | 294 | // Filter out bot/spam requests via UserAgent 295 | if (InstantAnalytics::$settings->filterBotUserAgents) { 296 | $crawlerDetect = new CrawlerDetect; 297 | // Check the user agent of the current 'visitor' 298 | if ($crawlerDetect->isCrawler()) { 299 | $this->logExclusion('filterBotUserAgents'); 300 | 301 | return false; 302 | } 303 | } 304 | 305 | // Filter by user group 306 | $userService = Craft::$app->getUser(); 307 | /** @var UserElement $user */ 308 | $user = $userService->getIdentity(); 309 | if ($user) { 310 | if (InstantAnalytics::$settings->adminExclude && $user->admin) { 311 | $this->logExclusion('adminExclude'); 312 | 313 | return false; 314 | } 315 | 316 | if (InstantAnalytics::$settings->groupExcludes !== null 317 | && is_array(InstantAnalytics::$settings->groupExcludes)) { 318 | foreach (InstantAnalytics::$settings->groupExcludes as $matchItem) { 319 | if ($user->isInGroup($matchItem)) { 320 | $this->logExclusion('groupExcludes'); 321 | 322 | return false; 323 | } 324 | } 325 | } 326 | } 327 | 328 | return $result; 329 | } 330 | 331 | /** 332 | * Log the reason for excluding the sending of analytics 333 | * 334 | * @param string $setting 335 | */ 336 | protected function logExclusion(string $setting) 337 | { 338 | if (InstantAnalytics::$settings->logExcludedAnalytics) { 339 | $request = Craft::$app->getRequest(); 340 | $requestIp = $request->getUserIP(); 341 | Craft::info( 342 | Craft::t( 343 | 'instant-analytics', 344 | 'Analytics excluded for:: {requestIp} due to: `{setting}`', 345 | [ 346 | 'requestIp' => $requestIp, 347 | 'setting' => $setting, 348 | ] 349 | ), 350 | __METHOD__ 351 | ); 352 | } 353 | } 354 | 355 | /** 356 | * Return a sanitized documentPath from a URL 357 | * 358 | * @param $url 359 | * 360 | * @return string 361 | */ 362 | protected function documentPathFromUrl($url = ''): string 363 | { 364 | if ($url === '') { 365 | $url = Craft::$app->getRequest()->getFullPath(); 366 | } 367 | 368 | // We want to send just a path to GA for page views 369 | if (UrlHelper::isAbsoluteUrl($url)) { 370 | $urlParts = parse_url($url); 371 | $url = $urlParts['path'] ?? '/'; 372 | if (isset($urlParts['query'])) { 373 | $url = $url . '?' . $urlParts['query']; 374 | } 375 | } 376 | 377 | // We don't want to send protocol-relative URLs either 378 | if (UrlHelper::isProtocolRelativeUrl($url)) { 379 | $url = substr($url, 1); 380 | } 381 | 382 | // Strip the query string if that's the global config setting 383 | if (InstantAnalytics::$settings) { 384 | if (InstantAnalytics::$settings->stripQueryString !== null 385 | && InstantAnalytics::$settings->stripQueryString) { 386 | $url = UrlHelper::stripQueryString($url); 387 | } 388 | } 389 | 390 | // We always want the path to be / rather than empty 391 | if ($url === '') { 392 | $url = '/'; 393 | } 394 | 395 | return $url; 396 | } 397 | 398 | /** 399 | * Get the Google Analytics object, primed with the default values 400 | * 401 | * @return null|IAnalytics object 402 | */ 403 | private function getAnalyticsObj() 404 | { 405 | $analytics = null; 406 | $request = Craft::$app->getRequest(); 407 | $trackingId = InstantAnalytics::$settings->googleAnalyticsTracking; 408 | if (!empty($trackingId)) { 409 | $trackingId = Craft::parseEnv($trackingId); 410 | } 411 | if (InstantAnalytics::$settings !== null 412 | && !empty($trackingId)) { 413 | $analytics = new IAnalytics(); 414 | if ($analytics) { 415 | $hostName = $request->getServerName(); 416 | if (empty($hostName)) { 417 | try { 418 | $hostName = parse_url(UrlHelper::siteUrl(), PHP_URL_HOST); 419 | } catch (Exception $e) { 420 | Craft::error( 421 | $e->getMessage(), 422 | __METHOD__ 423 | ); 424 | } 425 | } 426 | $userAgent = $request->getUserAgent(); 427 | if ($userAgent === null) { 428 | $userAgent = self::DEFAULT_USER_AGENT; 429 | } 430 | $referrer = $request->getReferrer(); 431 | if ($referrer === null) { 432 | $referrer = ''; 433 | } 434 | $analytics->setProtocolVersion('1') 435 | ->setTrackingId($trackingId) 436 | ->setIpOverride($request->getUserIP()) 437 | ->setUserAgentOverride($userAgent) 438 | ->setDocumentHostName($hostName) 439 | ->setDocumentReferrer($referrer) 440 | ->setAsyncRequest(false); 441 | 442 | // Try to parse a clientId from an existing _ga cookie 443 | $clientId = $this->gaParseCookie(); 444 | if (!empty($clientId)) { 445 | $analytics->setClientId($clientId); 446 | } 447 | // Set the gclid 448 | $gclid = $this->getGclid(); 449 | if ($gclid) { 450 | $analytics->setGoogleAdwordsId($gclid); 451 | } 452 | 453 | // Handle UTM parameters 454 | try { 455 | $session = Craft::$app->getSession(); 456 | } catch (MissingComponentException $e) { 457 | // That's ok 458 | $session = null; 459 | } 460 | // utm_source 461 | $utm_source = $request->getParam('utm_source') ?? $session->get('utm_source') ?? null; 462 | if (!empty($utm_source)) { 463 | $analytics->setCampaignSource($utm_source); 464 | if ($session) { 465 | $session->set('utm_source', $utm_source); 466 | } 467 | } 468 | // utm_medium 469 | $utm_medium = $request->getParam('utm_medium') ?? $session->get('utm_medium') ?? null; 470 | if (!empty($utm_medium)) { 471 | $analytics->setCampaignMedium($utm_medium); 472 | if ($session) { 473 | $session->set('utm_medium', $utm_medium); 474 | } 475 | } 476 | // utm_campaign 477 | $utm_campaign = $request->getParam('utm_campaign') ?? $session->get('utm_campaign') ?? null; 478 | if (!empty($utm_campaign)) { 479 | $analytics->setCampaignName($utm_campaign); 480 | if ($session) { 481 | $session->set('utm_campaign', $utm_campaign); 482 | } 483 | } 484 | // utm_content 485 | $utm_content = $request->getParam('utm_content') ?? $session->get('utm_content') ?? null; 486 | if (!empty($utm_content)) { 487 | $analytics->setCampaignContent($utm_content); 488 | if ($session) { 489 | $session->set('utm_content', $utm_content); 490 | } 491 | } 492 | 493 | // If SEOmatic is installed, set the affiliation as well 494 | if (InstantAnalytics::$seomaticPlugin && Seomatic::$settings->renderEnabled 495 | && Seomatic::$plugin->metaContainers->metaSiteVars !== null) { 496 | $siteName = Seomatic::$plugin->metaContainers->metaSiteVars->siteName; 497 | $analytics->setAffiliation($siteName); 498 | } 499 | } 500 | } 501 | 502 | return $analytics; 503 | } /* -- _getAnalyticsObj */ 504 | 505 | /** 506 | * _getGclid get the `gclid` and sets the 'gclid' cookie 507 | */ 508 | /** 509 | * _getGclid get the `gclid` and sets the 'gclid' cookie 510 | * 511 | * @return string 512 | */ 513 | private function getGclid(): string 514 | { 515 | $gclid = ''; 516 | if (isset($_GET['gclid'])) { 517 | $gclid = $_GET['gclid']; 518 | if (InstantAnalytics::$settings->createGclidCookie && !empty($gclid)) { 519 | setcookie('gclid', $gclid, strtotime('+10 years'), '/'); 520 | } 521 | } 522 | 523 | return $gclid; 524 | } 525 | 526 | /** 527 | * gaParseCookie handles the parsing of the _ga cookie or setting it to a 528 | * unique identifier 529 | * 530 | * @return string the cid 531 | */ 532 | private function gaParseCookie(): string 533 | { 534 | $cid = ''; 535 | if (isset($_COOKIE['_ga'])) { 536 | $parts = preg_split('[\.]', $_COOKIE['_ga'], 4); 537 | if ($parts !== false) { 538 | $cid = implode('.', array_slice($parts, 2)); 539 | } 540 | } elseif (isset($_COOKIE['_ia']) && $_COOKIE['_ia'] !== '') { 541 | $cid = $_COOKIE['_ia']; 542 | } else { 543 | // Only generate our own unique clientId if `requireGaCookieClientId` isn't true 544 | if (!InstantAnalytics::$settings->requireGaCookieClientId) { 545 | $cid = $this->gaGenUUID(); 546 | } 547 | } 548 | if (InstantAnalytics::$settings->createGclidCookie && !empty($cid)) { 549 | setcookie('_ia', $cid, strtotime('+2 years'), '/'); // Two years 550 | } 551 | 552 | return $cid; 553 | } 554 | 555 | /** 556 | * gaGenUUID Generate UUID v4 function - needed to generate a CID when one 557 | * isn't available 558 | * 559 | * @return string The generated UUID 560 | */ 561 | private function gaGenUUID() 562 | { 563 | return sprintf( 564 | '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', 565 | // 32 bits for "time_low" 566 | mt_rand(0, 0xffff), 567 | mt_rand(0, 0xffff), 568 | // 16 bits for "time_mid" 569 | mt_rand(0, 0xffff), 570 | // 16 bits for "time_hi_and_version", 571 | // four most significant bits holds version number 4 572 | mt_rand(0, 0x0fff) | 0x4000, 573 | // 16 bits, 8 bits for "clk_seq_hi_res", 574 | // 8 bits for "clk_seq_low", 575 | // two most significant bits holds zero and one for variant DCE1.1 576 | mt_rand(0, 0x3fff) | 0x8000, 577 | // 48 bits for "node" 578 | mt_rand(0, 0xffff), 579 | mt_rand(0, 0xffff), 580 | mt_rand(0, 0xffff) 581 | ); 582 | } 583 | } 584 | -------------------------------------------------------------------------------- /src/services/Commerce.php: -------------------------------------------------------------------------------- 1 | <?php 2 | /** 3 | * Instant Analytics plugin for Craft CMS 4 | * 5 | * Instant Analytics brings full Google Analytics support to your Twig templates 6 | * 7 | * @link https://nystudio107.com 8 | * @copyright Copyright (c) 2017 nystudio107 9 | */ 10 | 11 | namespace nystudio107\instantanalytics\services; 12 | 13 | use Craft; 14 | use craft\base\Component; 15 | use craft\commerce\base\Purchasable; 16 | use craft\commerce\elements\Order; 17 | use craft\commerce\elements\Product; 18 | use craft\commerce\elements\Variant; 19 | use craft\commerce\models\LineItem; 20 | use craft\elements\db\CategoryQuery; 21 | use craft\elements\db\MatrixBlockQuery; 22 | use craft\elements\db\TagQuery; 23 | use nystudio107\instantanalytics\helpers\IAnalytics; 24 | use nystudio107\instantanalytics\InstantAnalytics; 25 | 26 | /** 27 | * Commerce Service 28 | * 29 | * @author nystudio107 30 | * @package InstantAnalytics 31 | * @since 1.0.0 32 | */ 33 | class Commerce extends Component 34 | { 35 | // Public Methods 36 | // ========================================================================= 37 | 38 | /** 39 | * Send analytics information for the completed order 40 | * 41 | * @param Order $order the Product or Variant 42 | */ 43 | public function orderComplete($order = null) 44 | { 45 | if ($order) { 46 | $analytics = InstantAnalytics::$plugin->ia->eventAnalytics( 47 | 'Commerce', 48 | 'Purchase', 49 | $order->reference, 50 | $order->totalPrice 51 | ); 52 | 53 | if ($analytics) { 54 | $this->addCommerceOrderToAnalytics($analytics, $order); 55 | // Don't forget to set the product action, in this case to PURCHASE 56 | $analytics->setProductActionToPurchase(); 57 | $analytics->sendEvent(); 58 | 59 | Craft::info(Craft::t( 60 | 'instant-analytics', 61 | 'orderComplete for `Commerce` - `Purchase` - `{reference}` - `{price}`', 62 | ['reference' => $order->reference, 'price' => $order->totalPrice] 63 | ), __METHOD__); 64 | } 65 | } 66 | } 67 | 68 | /** 69 | * Send analytics information for the item added to the cart 70 | * 71 | * @param Order $order the Product or Variant 72 | * @param LineItem $lineItem the line item that was added 73 | */ 74 | public function addToCart( 75 | /** @noinspection PhpUnusedParameterInspection */ 76 | $order = null, $lineItem = null 77 | ) 78 | { 79 | if ($lineItem) { 80 | $title = $lineItem->purchasable->title ?? $lineItem->description; 81 | $quantity = $lineItem->qty; 82 | $analytics = InstantAnalytics::$plugin->ia->eventAnalytics('Commerce', 'Add to Cart', $title, $quantity); 83 | 84 | if ($analytics) { 85 | $title = $this->addProductDataFromLineItem($analytics, $lineItem); 86 | $analytics->setEventLabel($title); 87 | // Don't forget to set the product action, in this case to ADD 88 | $analytics->setProductActionToAdd(); 89 | $analytics->sendEvent(); 90 | 91 | Craft::info(Craft::t( 92 | 'instant-analytics', 93 | 'addToCart for `Commerce` - `Add to Cart` - `{title}` - `{quantity}`', 94 | ['title' => $title, 'quantity' => $quantity] 95 | ), __METHOD__); 96 | } 97 | } 98 | } 99 | 100 | /** 101 | * Send analytics information for the item removed from the cart 102 | * 103 | * @param Order|null $order 104 | * @param LineItem|null $lineItem 105 | */ 106 | public function removeFromCart( 107 | /** @noinspection PhpUnusedParameterInspection */ 108 | $order = null, $lineItem = null 109 | ) 110 | { 111 | if ($lineItem) { 112 | $title = $lineItem->purchasable->title ?? $lineItem->description; 113 | $quantity = $lineItem->qty; 114 | $analytics = InstantAnalytics::$plugin->ia->eventAnalytics( 115 | 'Commerce', 116 | 'Remove from Cart', 117 | $title, 118 | $quantity 119 | ); 120 | 121 | if ($analytics) { 122 | $title = $this->addProductDataFromLineItem($analytics, $lineItem); 123 | $analytics->setEventLabel($title); 124 | // Don't forget to set the product action, in this case to ADD 125 | $analytics->setProductActionToRemove(); 126 | $analytics->sendEvent(); 127 | 128 | Craft::info(Craft::t( 129 | 'instant-analytics', 130 | 'removeFromCart for `Commerce` - `Remove to Cart` - `{title}` - `{quantity}`', 131 | ['title' => $title, 'quantity' => $quantity] 132 | ), __METHOD__); 133 | } 134 | } 135 | } 136 | 137 | 138 | /** 139 | * Add a Craft Commerce OrderModel to an Analytics object 140 | * 141 | * @param IAnalytics $analytics the Analytics object 142 | * @param Order $order the Product or Variant 143 | */ 144 | public function addCommerceOrderToAnalytics($analytics = null, $order = null) 145 | { 146 | if ($order && $analytics) { 147 | // First, include the transaction data 148 | $analytics->setTransactionId($order->reference) 149 | ->setCurrencyCode($order->paymentCurrency) 150 | ->setRevenue($order->totalPrice) 151 | ->setTax($order->getTotalTax()) 152 | ->setShipping($order->getTotalShippingCost()); 153 | 154 | // Coupon code? 155 | if ($order->couponCode) { 156 | $analytics->setCouponCode($order->couponCode); 157 | } 158 | 159 | // Add each line item in the transaction 160 | // Two cases - variant and non variant products 161 | $index = 1; 162 | 163 | foreach ($order->lineItems as $key => $lineItem) { 164 | $this->addProductDataFromLineItem($analytics, $lineItem, $index, ''); 165 | $index++; 166 | } 167 | } 168 | } 169 | 170 | /** 171 | * Add a Craft Commerce LineItem to an Analytics object 172 | * 173 | * @param IAnalytics|null $analytics 174 | * @param LineItem|null $lineItem 175 | * @param int $index 176 | * @param string $listName 177 | * 178 | * @return string the title of the product 179 | * @throws \yii\base\InvalidConfigException 180 | */ 181 | public function addProductDataFromLineItem($analytics = null, $lineItem = null, $index = 0, $listName = ''): string 182 | { 183 | $result = ''; 184 | if ($lineItem && $analytics) { 185 | $product = null; 186 | $purchasable = $lineItem->purchasable; 187 | //This is the same for both variant and non variant products 188 | $productData = [ 189 | 'name' => $purchasable->title ?? $lineItem->description, 190 | 'sku' => $purchasable->sku ?? $lineItem->sku, 191 | 'price' => $lineItem->salePrice, 192 | 'quantity' => $lineItem->qty, 193 | ]; 194 | // Handle this purchasable being a Variant 195 | if (is_a($purchasable, Variant::class)) { 196 | /** @var Variant $purchasable */ 197 | $product = $purchasable->getProduct(); 198 | $variant = $purchasable; 199 | // Product with variants 200 | $productData['name'] = $product->title; 201 | $productData['variant'] = $variant->title; 202 | $productData['category'] = $product->getType(); 203 | } 204 | // Handle this purchasable being a Product 205 | if (is_a($purchasable, Product::class)) { 206 | /** @var Product $purchasable */ 207 | $product = $purchasable; 208 | $productData['name'] = $product->title; 209 | $productData['variant'] = $product->title; 210 | $productData['category'] = $product->getType(); 211 | } 212 | // Handle product lists 213 | if ($index) { 214 | $productData['position'] = $index; 215 | } 216 | if ($listName) { 217 | $productData['list'] = $listName; 218 | } 219 | // Add in any custom categories/brands that might be set 220 | if (InstantAnalytics::$settings && $product) { 221 | if (isset(InstantAnalytics::$settings['productCategoryField']) 222 | && !empty(InstantAnalytics::$settings['productCategoryField'])) { 223 | $productData['category'] = $this->pullDataFromField( 224 | $product, 225 | InstantAnalytics::$settings['productCategoryField'] 226 | ); 227 | } 228 | if (isset(InstantAnalytics::$settings['productBrandField']) 229 | && !empty(InstantAnalytics::$settings['productBrandField'])) { 230 | $productData['brand'] = $this->pullDataFromField( 231 | $product, 232 | InstantAnalytics::$settings['productBrandField'] 233 | ); 234 | } 235 | } 236 | $result = $productData['name']; 237 | //Add each product to the hit to be sent 238 | $analytics->addProduct($productData); 239 | } 240 | 241 | return $result; 242 | } 243 | 244 | /** 245 | * Add a product impression from a Craft Commerce Product or Variant 246 | * 247 | * @param IAnalytics $analytics the Analytics object 248 | * @param Product|Variant $productVariant the Product or Variant 249 | * @param int $index Where the product appears in the 250 | * list 251 | * @param string $listName 252 | * @param int $listIndex 253 | * 254 | * @throws \yii\base\InvalidConfigException 255 | */ 256 | public function addCommerceProductImpression( 257 | $analytics = null, 258 | $productVariant = null, 259 | $index = 0, 260 | $listName = 'default', 261 | $listIndex = 1 262 | ) 263 | { 264 | if ($productVariant && $analytics) { 265 | $productData = $this->getProductDataFromProduct($productVariant); 266 | 267 | /** 268 | * As per: https://github.com/theiconic/php-ga-measurement-protocol/issues/26 269 | */ 270 | if ($listName && $listIndex) { 271 | $analytics->setProductImpressionListName($listName, $listIndex); 272 | } 273 | 274 | if ($index) { 275 | $productData['position'] = $index; 276 | } 277 | 278 | //Add the product to the hit to be sent 279 | $analytics->addProductImpression($productData, $listIndex); 280 | 281 | Craft::info(Craft::t( 282 | 'instant-analytics', 283 | 'addCommerceProductImpression for `{sku}` - `{name}` - `{name}` - `{index}`', 284 | ['sku' => $productData['sku'], 'name' => $productData['name'], 'index' => $index] 285 | ), __METHOD__); 286 | } 287 | } 288 | 289 | /** 290 | * Add a product detail view from a Craft Commerce Product or Variant 291 | * 292 | * @param IAnalytics $analytics the Analytics object 293 | * @param Product|Variant $productVariant the Product or Variant 294 | * 295 | * @throws \yii\base\InvalidConfigException 296 | */ 297 | public function addCommerceProductDetailView($analytics = null, $productVariant = null) 298 | { 299 | if ($productVariant && $analytics) { 300 | $productData = $this->getProductDataFromProduct($productVariant); 301 | 302 | // Don't forget to set the product action, in this case to DETAIL 303 | $analytics->setProductActionToDetail(); 304 | 305 | //Add the product to the hit to be sent 306 | $analytics->addProduct($productData); 307 | 308 | Craft::info(Craft::t( 309 | 'instant-analytics', 310 | 'addCommerceProductDetailView for `{sku}` - `{name}`', 311 | ['sku' => $productData['sku'], 'name' => $productData['name']] 312 | ), __METHOD__); 313 | } 314 | } 315 | 316 | /** 317 | * Add a checkout step and option to an Analytics object 318 | * 319 | * @param IAnalytics $analytics the Analytics object 320 | * @param Order $order the Product or Variant 321 | * @param int $step the checkout step 322 | * @param string $option the checkout option 323 | */ 324 | public function addCommerceCheckoutStep($analytics = null, $order = null, $step = 1, $option = '') 325 | { 326 | if ($order && $analytics) { 327 | // Add each line item in the transaction 328 | // Two cases - variant and non variant products 329 | $index = 1; 330 | 331 | foreach ($order->lineItems as $key => $lineItem) { 332 | $this->addProductDataFromLineItem($analytics, $lineItem, $index, ''); 333 | $index++; 334 | } 335 | 336 | $analytics->setCheckoutStep($step); 337 | 338 | if ($option) { 339 | $analytics->setCheckoutStepOption($option); 340 | } 341 | 342 | // Don't forget to set the product action, in this case to CHECKOUT 343 | $analytics->setProductActionToCheckout(); 344 | 345 | Craft::info(Craft::t( 346 | 'instant-analytics', 347 | 'addCommerceCheckoutStep step: `{step}` with option: `{option}`', 348 | ['step' => $step, 'option' => $option] 349 | ), __METHOD__); 350 | } 351 | } 352 | 353 | /** 354 | * Extract product data from a Craft Commerce Product or Variant 355 | * 356 | * @param Product|Variant $productVariant the Product or Variant 357 | * 358 | * @return array the product data 359 | * @throws \yii\base\InvalidConfigException 360 | */ 361 | public function getProductDataFromProduct($productVariant = null): array 362 | { 363 | $result = []; 364 | 365 | // Extract the variant if it's a Product or Purchasable 366 | if ($productVariant && \is_object($productVariant)) { 367 | if (is_a($productVariant, Product::class) 368 | || is_a($productVariant, Purchasable::class) 369 | ) { 370 | $productType = property_exists($productVariant, 'typeId') 371 | ? InstantAnalytics::$commercePlugin->getProductTypes()->getProductTypeById($productVariant->typeId) 372 | : null; 373 | 374 | if ($productType && $productType->hasVariants) { 375 | $productVariants = $productVariant->getVariants(); 376 | $productVariant = reset($productVariants); 377 | $product = $productVariant->getProduct(); 378 | 379 | if ($product) { 380 | $category = $product->getType()['name']; 381 | $name = $product->title; 382 | $variant = $productVariant->title; 383 | } else { 384 | $category = $productVariant->getType()['name']; 385 | $name = $productVariant->title; 386 | $variant = ''; 387 | } 388 | } else { 389 | if (!empty($productVariant->defaultVariantId)) { 390 | /** @var Variant $productVariant */ 391 | $productVariant = InstantAnalytics::$commercePlugin->getVariants()->getVariantById( 392 | $productVariant->defaultVariantId 393 | ); 394 | $category = $productVariant->getProduct()->getType()['name']; 395 | $name = $productVariant->title; 396 | $variant = ''; 397 | } else { 398 | if (isset($productVariant->product)) { 399 | $category = $productVariant->product->getType()['name']; 400 | $name = $productVariant->product->title; 401 | } else { 402 | $category = $productVariant->getType()['name']; 403 | $name = $productVariant->title; 404 | } 405 | $variant = $productVariant->title; 406 | } 407 | } 408 | } 409 | 410 | $productData = [ 411 | 'sku' => $productVariant->sku, 412 | 'name' => $name, 413 | 'price' => number_format($productVariant->price, 2, '.', ''), 414 | 'category' => $category, 415 | ]; 416 | 417 | if ($variant) { 418 | $productData['variant'] = $variant; 419 | } 420 | 421 | $isVariant = is_a($productVariant, Variant::class); 422 | 423 | if (InstantAnalytics::$settings) { 424 | if (isset(InstantAnalytics::$settings['productCategoryField']) 425 | && !empty(InstantAnalytics::$settings['productCategoryField'])) { 426 | $productData['category'] = $this->pullDataFromField( 427 | $productVariant, 428 | InstantAnalytics::$settings['productCategoryField'] 429 | ); 430 | if (empty($productData['category']) && $isVariant) { 431 | $productData['category'] = $this->pullDataFromField( 432 | $productVariant->product, 433 | InstantAnalytics::$settings['productCategoryField'] 434 | ); 435 | } 436 | } 437 | if (isset(InstantAnalytics::$settings['productBrandField']) 438 | && !empty(InstantAnalytics::$settings['productBrandField'])) { 439 | $productData['brand'] = $this->pullDataFromField( 440 | $productVariant, 441 | InstantAnalytics::$settings['productBrandField'], 442 | true 443 | ); 444 | 445 | if (empty($productData['brand']) && $isVariant) { 446 | $productData['brand'] = $this->pullDataFromField( 447 | $productVariant, 448 | InstantAnalytics::$settings['productBrandField'], 449 | true 450 | ); 451 | } 452 | } 453 | } 454 | 455 | $result = $productData; 456 | } 457 | 458 | return $result; 459 | } 460 | 461 | /** 462 | * @param Product|Variant|null $productVariant 463 | * @param string $fieldHandle 464 | * @param bool $isBrand 465 | * 466 | * @return string 467 | */ 468 | protected function pullDataFromField($productVariant, $fieldHandle, $isBrand = false): string 469 | { 470 | $result = ''; 471 | if ($productVariant && $fieldHandle) { 472 | $srcField = $productVariant[$fieldHandle] ?? $productVariant->product[$fieldHandle] ?? null; 473 | // Handle eager loaded elements 474 | if (is_array($srcField)) { 475 | return $this->getDataFromElements($isBrand, $srcField); 476 | } 477 | // If the source field isn't an object, return nothing 478 | if (!is_object($srcField)) { 479 | return $result; 480 | } 481 | switch (\get_class($srcField)) { 482 | case MatrixBlockQuery::class: 483 | break; 484 | case TagQuery::class: 485 | break; 486 | case CategoryQuery::class: 487 | $result = $this->getDataFromElements($isBrand, $srcField->all()); 488 | break; 489 | 490 | 491 | default: 492 | $result = strip_tags($srcField); 493 | break; 494 | } 495 | } 496 | 497 | return $result; 498 | } 499 | 500 | /** 501 | * @param bool $isBrand 502 | * @param array $elements 503 | * @return string 504 | */ 505 | protected function getDataFromElements(bool $isBrand, array $elements): string 506 | { 507 | $cats = []; 508 | 509 | if ($isBrand) { 510 | // Because we can only have one brand, we'll get 511 | // the very last category. This means if our 512 | // brand is a sub-category, we'll get the child 513 | // not the parent. 514 | foreach ($elements as $cat) { 515 | $cats = [$cat->title]; 516 | } 517 | } else { 518 | // For every category, show its ancestors 519 | // delimited by a slash. 520 | foreach ($elements as $cat) { 521 | $name = $cat->title; 522 | 523 | while ($cat = $cat->parent) { 524 | $name = $cat->title . '/' . $name; 525 | } 526 | 527 | $cats[] = $name; 528 | } 529 | } 530 | 531 | // Join separate categories with a pipe. 532 | return implode('|', $cats); 533 | } 534 | } 535 | -------------------------------------------------------------------------------- /src/web/assets/dist/assets/welcome-71d12b53.js: -------------------------------------------------------------------------------- 1 | function mn(e,t){const n=Object.create(null),s=e.split(",");for(let i=0;i<s.length;i++)n[s[i]]=!0;return t?i=>!!n[i.toLowerCase()]:i=>!!n[i]}function _n(e){if(A(e)){const t={};for(let n=0;n<e.length;n++){const s=e[n],i=X(s)?mi(s):_n(s);if(i)for(const r in i)t[r]=i[r]}return t}else{if(X(e))return e;if(J(e))return e}}const di=/;(?![^(]*\))/g,pi=/:([^]+)/,gi=/\/\*.*?\*\//gs;function mi(e){const t={};return e.replace(gi,"").split(di).forEach(n=>{if(n){const s=n.split(pi);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function bn(e){let t="";if(X(e))t=e;else if(A(e))for(let n=0;n<e.length;n++){const s=bn(e[n]);s&&(t+=s+" ")}else if(J(e))for(const n in e)e[n]&&(t+=n+" ");return t.trim()}const _i="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly",bi=mn(_i);function xs(e){return!!e||e===""}const $={},Qe=[],ge=()=>{},xi=()=>!1,wi=/^on[^a-z]/,Rt=e=>wi.test(e),xn=e=>e.startsWith("onUpdate:"),G=Object.assign,wn=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},yi=Object.prototype.hasOwnProperty,H=(e,t)=>yi.call(e,t),A=Array.isArray,ft=e=>Ht(e)==="[object Map]",vi=e=>Ht(e)==="[object Set]",F=e=>typeof e=="function",X=e=>typeof e=="string",yn=e=>typeof e=="symbol",J=e=>e!==null&&typeof e=="object",ws=e=>J(e)&&F(e.then)&&F(e.catch),Ci=Object.prototype.toString,Ht=e=>Ci.call(e),Ei=e=>Ht(e).slice(8,-1),Ti=e=>Ht(e)==="[object Object]",vn=e=>X(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Tt=mn(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),Bt=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},Mi=/-(\w)/g,tt=Bt(e=>e.replace(Mi,(t,n)=>n?n.toUpperCase():"")),Ii=/\B([A-Z])/g,st=Bt(e=>e.replace(Ii,"-$1").toLowerCase()),ys=Bt(e=>e.charAt(0).toUpperCase()+e.slice(1)),qt=Bt(e=>e?`on${ys(e)}`:""),Pt=(e,t)=>!Object.is(e,t),Vt=(e,t)=>{for(let n=0;n<e.length;n++)e[n](t)},At=(e,t,n)=>{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,value:n})},Oi=e=>{const t=parseFloat(e);return isNaN(t)?e:t};let qn;const Pi=()=>qn||(qn=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});let ue;class Ai{constructor(t=!1){this.detached=t,this._active=!0,this.effects=[],this.cleanups=[],this.parent=ue,!t&&ue&&(this.index=(ue.scopes||(ue.scopes=[])).push(this)-1)}get active(){return this._active}run(t){if(this._active){const n=ue;try{return ue=this,t()}finally{ue=n}}}on(){ue=this}off(){ue=this.parent}stop(t){if(this._active){let n,s;for(n=0,s=this.effects.length;n<s;n++)this.effects[n].stop();for(n=0,s=this.cleanups.length;n<s;n++)this.cleanups[n]();if(this.scopes)for(n=0,s=this.scopes.length;n<s;n++)this.scopes[n].stop(!0);if(!this.detached&&this.parent&&!t){const i=this.parent.scopes.pop();i&&i!==this&&(this.parent.scopes[this.index]=i,i.index=this.index)}this.parent=void 0,this._active=!1}}}function Fi(e,t=ue){t&&t.active&&t.effects.push(e)}function Si(){return ue}const Cn=e=>{const t=new Set(e);return t.w=0,t.n=0,t},vs=e=>(e.w&Le)>0,Cs=e=>(e.n&Le)>0,Ri=({deps:e})=>{if(e.length)for(let t=0;t<e.length;t++)e[t].w|=Le},Hi=e=>{const{deps:t}=e;if(t.length){let n=0;for(let s=0;s<t.length;s++){const i=t[s];vs(i)&&!Cs(i)?i.delete(e):t[n++]=i,i.w&=~Le,i.n&=~Le}t.length=n}},tn=new WeakMap;let ct=0,Le=1;const nn=30;let he;const qe=Symbol(""),sn=Symbol("");class En{constructor(t,n=null,s){this.fn=t,this.scheduler=n,this.active=!0,this.deps=[],this.parent=void 0,Fi(this,s)}run(){if(!this.active)return this.fn();let t=he,n=Re;for(;t;){if(t===this)return;t=t.parent}try{return this.parent=he,he=this,Re=!0,Le=1<<++ct,ct<=nn?Ri(this):Vn(this),this.fn()}finally{ct<=nn&&Hi(this),Le=1<<--ct,he=this.parent,Re=n,this.parent=void 0,this.deferStop&&this.stop()}}stop(){he===this?this.deferStop=!0:this.active&&(Vn(this),this.onStop&&this.onStop(),this.active=!1)}}function Vn(e){const{deps:t}=e;if(t.length){for(let n=0;n<t.length;n++)t[n].delete(e);t.length=0}}let Re=!0;const Es=[];function it(){Es.push(Re),Re=!1}function rt(){const e=Es.pop();Re=e===void 0?!0:e}function ie(e,t,n){if(Re&&he){let s=tn.get(e);s||tn.set(e,s=new Map);let i=s.get(n);i||s.set(n,i=Cn()),Ts(i)}}function Ts(e,t){let n=!1;ct<=nn?Cs(e)||(e.n|=Le,n=!vs(e)):n=!e.has(he),n&&(e.add(he),he.deps.push(e))}function Oe(e,t,n,s,i,r){const l=tn.get(e);if(!l)return;let c=[];if(t==="clear")c=[...l.values()];else if(n==="length"&&A(e)){const a=Number(s);l.forEach((h,g)=>{(g==="length"||g>=a)&&c.push(h)})}else switch(n!==void 0&&c.push(l.get(n)),t){case"add":A(e)?vn(n)&&c.push(l.get("length")):(c.push(l.get(qe)),ft(e)&&c.push(l.get(sn)));break;case"delete":A(e)||(c.push(l.get(qe)),ft(e)&&c.push(l.get(sn)));break;case"set":ft(e)&&c.push(l.get(qe));break}if(c.length===1)c[0]&&rn(c[0]);else{const a=[];for(const h of c)h&&a.push(...h);rn(Cn(a))}}function rn(e,t){const n=A(e)?e:[...e];for(const s of n)s.computed&&Jn(s);for(const s of n)s.computed||Jn(s)}function Jn(e,t){(e!==he||e.allowRecurse)&&(e.scheduler?e.scheduler():e.run())}const Bi=mn("__proto__,__v_isRef,__isVue"),Ms=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(yn)),Li=Tn(),Ni=Tn(!1,!0),Di=Tn(!0),Yn=ji();function ji(){const e={};return["includes","indexOf","lastIndexOf"].forEach(t=>{e[t]=function(...n){const s=B(this);for(let r=0,l=this.length;r<l;r++)ie(s,"get",r+"");const i=s[t](...n);return i===-1||i===!1?s[t](...n.map(B)):i}}),["push","pop","shift","unshift","splice"].forEach(t=>{e[t]=function(...n){it();const s=B(this)[t].apply(this,n);return rt(),s}}),e}function Ui(e){const t=B(this);return ie(t,"has",e),t.hasOwnProperty(e)}function Tn(e=!1,t=!1){return function(s,i,r){if(i==="__v_isReactive")return!e;if(i==="__v_isReadonly")return e;if(i==="__v_isShallow")return t;if(i==="__v_raw"&&r===(e?t?nr:Fs:t?As:Ps).get(s))return s;const l=A(s);if(!e){if(l&&H(Yn,i))return Reflect.get(Yn,i,r);if(i==="hasOwnProperty")return Ui}const c=Reflect.get(s,i,r);return(yn(i)?Ms.has(i):Bi(i))||(e||ie(s,"get",i),t)?c:ne(c)?l&&vn(i)?c:c.value:J(c)?e?Ss(c):On(c):c}}const $i=Is(),Ki=Is(!0);function Is(e=!1){return function(n,s,i,r){let l=n[s];if(ht(l)&&ne(l)&&!ne(i))return!1;if(!e&&(!ln(i)&&!ht(i)&&(l=B(l),i=B(i)),!A(n)&&ne(l)&&!ne(i)))return l.value=i,!0;const c=A(n)&&vn(s)?Number(s)<n.length:H(n,s),a=Reflect.set(n,s,i,r);return n===B(r)&&(c?Pt(i,l)&&Oe(n,"set",s,i):Oe(n,"add",s,i)),a}}function zi(e,t){const n=H(e,t);e[t];const s=Reflect.deleteProperty(e,t);return s&&n&&Oe(e,"delete",t,void 0),s}function Wi(e,t){const n=Reflect.has(e,t);return(!yn(t)||!Ms.has(t))&&ie(e,"has",t),n}function ki(e){return ie(e,"iterate",A(e)?"length":qe),Reflect.ownKeys(e)}const Os={get:Li,set:$i,deleteProperty:zi,has:Wi,ownKeys:ki},qi={get:Di,set(e,t){return!0},deleteProperty(e,t){return!0}},Vi=G({},Os,{get:Ni,set:Ki}),Mn=e=>e,Lt=e=>Reflect.getPrototypeOf(e);function xt(e,t,n=!1,s=!1){e=e.__v_raw;const i=B(e),r=B(t);n||(t!==r&&ie(i,"get",t),ie(i,"get",r));const{has:l}=Lt(i),c=s?Mn:n?Fn:An;if(l.call(i,t))return c(e.get(t));if(l.call(i,r))return c(e.get(r));e!==i&&e.get(t)}function wt(e,t=!1){const n=this.__v_raw,s=B(n),i=B(e);return t||(e!==i&&ie(s,"has",e),ie(s,"has",i)),e===i?n.has(e):n.has(e)||n.has(i)}function yt(e,t=!1){return e=e.__v_raw,!t&&ie(B(e),"iterate",qe),Reflect.get(e,"size",e)}function Xn(e){e=B(e);const t=B(this);return Lt(t).has.call(t,e)||(t.add(e),Oe(t,"add",e,e)),this}function Zn(e,t){t=B(t);const n=B(this),{has:s,get:i}=Lt(n);let r=s.call(n,e);r||(e=B(e),r=s.call(n,e));const l=i.call(n,e);return n.set(e,t),r?Pt(t,l)&&Oe(n,"set",e,t):Oe(n,"add",e,t),this}function Qn(e){const t=B(this),{has:n,get:s}=Lt(t);let i=n.call(t,e);i||(e=B(e),i=n.call(t,e)),s&&s.call(t,e);const r=t.delete(e);return i&&Oe(t,"delete",e,void 0),r}function Gn(){const e=B(this),t=e.size!==0,n=e.clear();return t&&Oe(e,"clear",void 0,void 0),n}function vt(e,t){return function(s,i){const r=this,l=r.__v_raw,c=B(l),a=t?Mn:e?Fn:An;return!e&&ie(c,"iterate",qe),l.forEach((h,g)=>s.call(i,a(h),a(g),r))}}function Ct(e,t,n){return function(...s){const i=this.__v_raw,r=B(i),l=ft(r),c=e==="entries"||e===Symbol.iterator&&l,a=e==="keys"&&l,h=i[e](...s),g=n?Mn:t?Fn:An;return!t&&ie(r,"iterate",a?sn:qe),{next(){const{value:w,done:v}=h.next();return v?{value:w,done:v}:{value:c?[g(w[0]),g(w[1])]:g(w),done:v}},[Symbol.iterator](){return this}}}}function Fe(e){return function(...t){return e==="delete"?!1:this}}function Ji(){const e={get(r){return xt(this,r)},get size(){return yt(this)},has:wt,add:Xn,set:Zn,delete:Qn,clear:Gn,forEach:vt(!1,!1)},t={get(r){return xt(this,r,!1,!0)},get size(){return yt(this)},has:wt,add:Xn,set:Zn,delete:Qn,clear:Gn,forEach:vt(!1,!0)},n={get(r){return xt(this,r,!0)},get size(){return yt(this,!0)},has(r){return wt.call(this,r,!0)},add:Fe("add"),set:Fe("set"),delete:Fe("delete"),clear:Fe("clear"),forEach:vt(!0,!1)},s={get(r){return xt(this,r,!0,!0)},get size(){return yt(this,!0)},has(r){return wt.call(this,r,!0)},add:Fe("add"),set:Fe("set"),delete:Fe("delete"),clear:Fe("clear"),forEach:vt(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(r=>{e[r]=Ct(r,!1,!1),n[r]=Ct(r,!0,!1),t[r]=Ct(r,!1,!0),s[r]=Ct(r,!0,!0)}),[e,n,t,s]}const[Yi,Xi,Zi,Qi]=Ji();function In(e,t){const n=t?e?Qi:Zi:e?Xi:Yi;return(s,i,r)=>i==="__v_isReactive"?!e:i==="__v_isReadonly"?e:i==="__v_raw"?s:Reflect.get(H(n,i)&&i in s?n:s,i,r)}const Gi={get:In(!1,!1)},er={get:In(!1,!0)},tr={get:In(!0,!1)},Ps=new WeakMap,As=new WeakMap,Fs=new WeakMap,nr=new WeakMap;function sr(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function ir(e){return e.__v_skip||!Object.isExtensible(e)?0:sr(Ei(e))}function On(e){return ht(e)?e:Pn(e,!1,Os,Gi,Ps)}function rr(e){return Pn(e,!1,Vi,er,As)}function Ss(e){return Pn(e,!0,qi,tr,Fs)}function Pn(e,t,n,s,i){if(!J(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const r=i.get(e);if(r)return r;const l=ir(e);if(l===0)return e;const c=new Proxy(e,l===2?s:n);return i.set(e,c),c}function Ge(e){return ht(e)?Ge(e.__v_raw):!!(e&&e.__v_isReactive)}function ht(e){return!!(e&&e.__v_isReadonly)}function ln(e){return!!(e&&e.__v_isShallow)}function Rs(e){return Ge(e)||ht(e)}function B(e){const t=e&&e.__v_raw;return t?B(t):e}function Hs(e){return At(e,"__v_skip",!0),e}const An=e=>J(e)?On(e):e,Fn=e=>J(e)?Ss(e):e;function lr(e){Re&&he&&(e=B(e),Ts(e.dep||(e.dep=Cn())))}function or(e,t){e=B(e);const n=e.dep;n&&rn(n)}function ne(e){return!!(e&&e.__v_isRef===!0)}function cr(e){return ne(e)?e.value:e}const fr={get:(e,t,n)=>cr(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const i=e[t];return ne(i)&&!ne(n)?(i.value=n,!0):Reflect.set(e,t,n,s)}};function Bs(e){return Ge(e)?e:new Proxy(e,fr)}var Ls;class ar{constructor(t,n,s,i){this._setter=n,this.dep=void 0,this.__v_isRef=!0,this[Ls]=!1,this._dirty=!0,this.effect=new En(t,()=>{this._dirty||(this._dirty=!0,or(this))}),this.effect.computed=this,this.effect.active=this._cacheable=!i,this.__v_isReadonly=s}get value(){const t=B(this);return lr(t),(t._dirty||!t._cacheable)&&(t._dirty=!1,t._value=t.effect.run()),t._value}set value(t){this._setter(t)}}Ls="__v_isReadonly";function ur(e,t,n=!1){let s,i;const r=F(e);return r?(s=e,i=ge):(s=e.get,i=e.set),new ar(s,i,r||!i,n)}function He(e,t,n,s){let i;try{i=s?e(...s):e()}catch(r){Nt(r,t,n)}return i}function fe(e,t,n,s){if(F(e)){const r=He(e,t,n,s);return r&&ws(r)&&r.catch(l=>{Nt(l,t,n)}),r}const i=[];for(let r=0;r<e.length;r++)i.push(fe(e[r],t,n,s));return i}function Nt(e,t,n,s=!0){const i=t?t.vnode:null;if(t){let r=t.parent;const l=t.proxy,c=n;for(;r;){const h=r.ec;if(h){for(let g=0;g<h.length;g++)if(h[g](e,l,c)===!1)return}r=r.parent}const a=t.appContext.config.errorHandler;if(a){He(a,null,10,[e,l,c]);return}}hr(e,n,i,s)}function hr(e,t,n,s=!0){console.error(e)}let dt=!1,on=!1;const Q=[];let ve=0;const et=[];let Me=null,ze=0;const Ns=Promise.resolve();let Sn=null;function dr(e){const t=Sn||Ns;return e?t.then(this?e.bind(this):e):t}function pr(e){let t=ve+1,n=Q.length;for(;t<n;){const s=t+n>>>1;pt(Q[s])<e?t=s+1:n=s}return t}function Rn(e){(!Q.length||!Q.includes(e,dt&&e.allowRecurse?ve+1:ve))&&(e.id==null?Q.push(e):Q.splice(pr(e.id),0,e),Ds())}function Ds(){!dt&&!on&&(on=!0,Sn=Ns.then(Us))}function gr(e){const t=Q.indexOf(e);t>ve&&Q.splice(t,1)}function mr(e){A(e)?et.push(...e):(!Me||!Me.includes(e,e.allowRecurse?ze+1:ze))&&et.push(e),Ds()}function es(e,t=dt?ve+1:0){for(;t<Q.length;t++){const n=Q[t];n&&n.pre&&(Q.splice(t,1),t--,n())}}function js(e){if(et.length){const t=[...new Set(et)];if(et.length=0,Me){Me.push(...t);return}for(Me=t,Me.sort((n,s)=>pt(n)-pt(s)),ze=0;ze<Me.length;ze++)Me[ze]();Me=null,ze=0}}const pt=e=>e.id==null?1/0:e.id,_r=(e,t)=>{const n=pt(e)-pt(t);if(n===0){if(e.pre&&!t.pre)return-1;if(t.pre&&!e.pre)return 1}return n};function Us(e){on=!1,dt=!0,Q.sort(_r);const t=ge;try{for(ve=0;ve<Q.length;ve++){const n=Q[ve];n&&n.active!==!1&&He(n,null,14)}}finally{ve=0,Q.length=0,js(),dt=!1,Sn=null,(Q.length||et.length)&&Us()}}function br(e,t,...n){if(e.isUnmounted)return;const s=e.vnode.props||$;let i=n;const r=t.startsWith("update:"),l=r&&t.slice(7);if(l&&l in s){const g=`${l==="modelValue"?"model":l}Modifiers`,{number:w,trim:v}=s[g]||$;v&&(i=n.map(O=>X(O)?O.trim():O)),w&&(i=n.map(Oi))}let c,a=s[c=qt(t)]||s[c=qt(tt(t))];!a&&r&&(a=s[c=qt(st(t))]),a&&fe(a,e,6,i);const h=s[c+"Once"];if(h){if(!e.emitted)e.emitted={};else if(e.emitted[c])return;e.emitted[c]=!0,fe(h,e,6,i)}}function $s(e,t,n=!1){const s=t.emitsCache,i=s.get(e);if(i!==void 0)return i;const r=e.emits;let l={},c=!1;if(!F(e)){const a=h=>{const g=$s(h,t,!0);g&&(c=!0,G(l,g))};!n&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}return!r&&!c?(J(e)&&s.set(e,null),null):(A(r)?r.forEach(a=>l[a]=null):G(l,r),J(e)&&s.set(e,l),l)}function Dt(e,t){return!e||!Rt(t)?!1:(t=t.slice(2).replace(/Once$/,""),H(e,t[0].toLowerCase()+t.slice(1))||H(e,st(t))||H(e,t))}let de=null,Ks=null;function Ft(e){const t=de;return de=e,Ks=e&&e.type.__scopeId||null,t}function xr(e,t=de,n){if(!t||e._n)return e;const s=(...i)=>{s._d&&fs(-1);const r=Ft(t);let l;try{l=e(...i)}finally{Ft(r),s._d&&fs(1)}return l};return s._n=!0,s._c=!0,s._d=!0,s}function Jt(e){const{type:t,vnode:n,proxy:s,withProxy:i,props:r,propsOptions:[l],slots:c,attrs:a,emit:h,render:g,renderCache:w,data:v,setupState:O,ctx:L,inheritAttrs:M}=e;let k,D;const oe=Ft(e);try{if(n.shapeFlag&4){const K=i||s;k=ye(g.call(K,K,w,r,O,v,L)),D=a}else{const K=t;k=ye(K.length>1?K(r,{attrs:a,slots:c,emit:h}):K(r,null)),D=t.props?a:wr(a)}}catch(K){ut.length=0,Nt(K,e,1),k=Be(Ie)}let P=k;if(D&&M!==!1){const K=Object.keys(D),{shapeFlag:Z}=P;K.length&&Z&7&&(l&&K.some(xn)&&(D=yr(D,l)),P=Ne(P,D))}return n.dirs&&(P=Ne(P),P.dirs=P.dirs?P.dirs.concat(n.dirs):n.dirs),n.transition&&(P.transition=n.transition),k=P,Ft(oe),k}const wr=e=>{let t;for(const n in e)(n==="class"||n==="style"||Rt(n))&&((t||(t={}))[n]=e[n]);return t},yr=(e,t)=>{const n={};for(const s in e)(!xn(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function vr(e,t,n){const{props:s,children:i,component:r}=e,{props:l,children:c,patchFlag:a}=t,h=r.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&a>=0){if(a&1024)return!0;if(a&16)return s?ts(s,l,h):!!l;if(a&8){const g=t.dynamicProps;for(let w=0;w<g.length;w++){const v=g[w];if(l[v]!==s[v]&&!Dt(h,v))return!0}}}else return(i||c)&&(!c||!c.$stable)?!0:s===l?!1:s?l?ts(s,l,h):!0:!!l;return!1}function ts(e,t,n){const s=Object.keys(t);if(s.length!==Object.keys(e).length)return!0;for(let i=0;i<s.length;i++){const r=s[i];if(t[r]!==e[r]&&!Dt(n,r))return!0}return!1}function Cr({vnode:e,parent:t},n){for(;t&&t.subTree===e;)(e=t.vnode).el=n,t=t.parent}const Er=e=>e.__isSuspense;function Tr(e,t){t&&t.pendingBranch?A(e)?t.effects.push(...e):t.effects.push(e):mr(e)}function Mr(e,t){if(V){let n=V.provides;const s=V.parent&&V.parent.provides;s===n&&(n=V.provides=Object.create(s)),n[e]=t}}function Mt(e,t,n=!1){const s=V||de;if(s){const i=s.parent==null?s.vnode.appContext&&s.vnode.appContext.provides:s.parent.provides;if(i&&e in i)return i[e];if(arguments.length>1)return n&&F(t)?t.call(s.proxy):t}}const Et={};function Yt(e,t,n){return zs(e,t,n)}function zs(e,t,{immediate:n,deep:s,flush:i,onTrack:r,onTrigger:l}=$){const c=Si()===(V==null?void 0:V.scope)?V:null;let a,h=!1,g=!1;if(ne(e)?(a=()=>e.value,h=ln(e)):Ge(e)?(a=()=>e,s=!0):A(e)?(g=!0,h=e.some(P=>Ge(P)||ln(P)),a=()=>e.map(P=>{if(ne(P))return P.value;if(Ge(P))return Ze(P);if(F(P))return He(P,c,2)})):F(e)?t?a=()=>He(e,c,2):a=()=>{if(!(c&&c.isUnmounted))return w&&w(),fe(e,c,3,[v])}:a=ge,t&&s){const P=a;a=()=>Ze(P())}let w,v=P=>{w=D.onStop=()=>{He(P,c,4)}},O;if(mt)if(v=ge,t?n&&fe(t,c,3,[a(),g?[]:void 0,v]):a(),i==="sync"){const P=El();O=P.__watcherHandles||(P.__watcherHandles=[])}else return ge;let L=g?new Array(e.length).fill(Et):Et;const M=()=>{if(D.active)if(t){const P=D.run();(s||h||(g?P.some((K,Z)=>Pt(K,L[Z])):Pt(P,L)))&&(w&&w(),fe(t,c,3,[P,L===Et?void 0:g&&L[0]===Et?[]:L,v]),L=P)}else D.run()};M.allowRecurse=!!t;let k;i==="sync"?k=M:i==="post"?k=()=>se(M,c&&c.suspense):(M.pre=!0,c&&(M.id=c.uid),k=()=>Rn(M));const D=new En(a,k);t?n?M():L=D.run():i==="post"?se(D.run.bind(D),c&&c.suspense):D.run();const oe=()=>{D.stop(),c&&c.scope&&wn(c.scope.effects,D)};return O&&O.push(oe),oe}function Ir(e,t,n){const s=this.proxy,i=X(e)?e.includes(".")?Ws(s,e):()=>s[e]:e.bind(s,s);let r;F(t)?r=t:(r=t.handler,n=t);const l=V;nt(this);const c=zs(i,r.bind(s),n);return l?nt(l):Ve(),c}function Ws(e,t){const n=t.split(".");return()=>{let s=e;for(let i=0;i<n.length&&s;i++)s=s[n[i]];return s}}function Ze(e,t){if(!J(e)||e.__v_skip||(t=t||new Set,t.has(e)))return e;if(t.add(e),ne(e))Ze(e.value,t);else if(A(e))for(let n=0;n<e.length;n++)Ze(e[n],t);else if(vi(e)||ft(e))e.forEach(n=>{Ze(n,t)});else if(Ti(e))for(const n in e)Ze(e[n],t);return e}function Or(){const e={isMounted:!1,isLeaving:!1,isUnmounting:!1,leavingVNodes:new Map};return Hn(()=>{e.isMounted=!0}),Ys(()=>{e.isUnmounting=!0}),e}const ce=[Function,Array],Pr={name:"BaseTransition",props:{mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:ce,onEnter:ce,onAfterEnter:ce,onEnterCancelled:ce,onBeforeLeave:ce,onLeave:ce,onAfterLeave:ce,onLeaveCancelled:ce,onBeforeAppear:ce,onAppear:ce,onAfterAppear:ce,onAppearCancelled:ce},setup(e,{slots:t}){const n=ml(),s=Or();let i;return()=>{const r=t.default&&qs(t.default(),!0);if(!r||!r.length)return;let l=r[0];if(r.length>1){for(const M of r)if(M.type!==Ie){l=M;break}}const c=B(e),{mode:a}=c;if(s.isLeaving)return Xt(l);const h=ns(l);if(!h)return Xt(l);const g=cn(h,c,s,n);fn(h,g);const w=n.subTree,v=w&&ns(w);let O=!1;const{getTransitionKey:L}=h.type;if(L){const M=L();i===void 0?i=M:M!==i&&(i=M,O=!0)}if(v&&v.type!==Ie&&(!We(h,v)||O)){const M=cn(v,c,s,n);if(fn(v,M),a==="out-in")return s.isLeaving=!0,M.afterLeave=()=>{s.isLeaving=!1,n.update.active!==!1&&n.update()},Xt(l);a==="in-out"&&h.type!==Ie&&(M.delayLeave=(k,D,oe)=>{const P=ks(s,v);P[String(v.key)]=v,k._leaveCb=()=>{D(),k._leaveCb=void 0,delete g.delayedLeave},g.delayedLeave=oe})}return l}}},Ar=Pr;function ks(e,t){const{leavingVNodes:n}=e;let s=n.get(t.type);return s||(s=Object.create(null),n.set(t.type,s)),s}function cn(e,t,n,s){const{appear:i,mode:r,persisted:l=!1,onBeforeEnter:c,onEnter:a,onAfterEnter:h,onEnterCancelled:g,onBeforeLeave:w,onLeave:v,onAfterLeave:O,onLeaveCancelled:L,onBeforeAppear:M,onAppear:k,onAfterAppear:D,onAppearCancelled:oe}=t,P=String(e.key),K=ks(n,e),Z=(S,Y)=>{S&&fe(S,s,9,Y)},Je=(S,Y)=>{const z=Y[1];Z(S,Y),A(S)?S.every(re=>re.length<=1)&&z():S.length<=1&&z()},Ae={mode:r,persisted:l,beforeEnter(S){let Y=c;if(!n.isMounted)if(i)Y=M||c;else return;S._leaveCb&&S._leaveCb(!0);const z=K[P];z&&We(e,z)&&z.el._leaveCb&&z.el._leaveCb(),Z(Y,[S])},enter(S){let Y=a,z=h,re=g;if(!n.isMounted)if(i)Y=k||a,z=D||h,re=oe||g;else return;let me=!1;const Ce=S._enterCb=lt=>{me||(me=!0,lt?Z(re,[S]):Z(z,[S]),Ae.delayedLeave&&Ae.delayedLeave(),S._enterCb=void 0)};Y?Je(Y,[S,Ce]):Ce()},leave(S,Y){const z=String(e.key);if(S._enterCb&&S._enterCb(!0),n.isUnmounting)return Y();Z(w,[S]);let re=!1;const me=S._leaveCb=Ce=>{re||(re=!0,Y(),Ce?Z(L,[S]):Z(O,[S]),S._leaveCb=void 0,K[z]===e&&delete K[z])};K[z]=e,v?Je(v,[S,me]):me()},clone(S){return cn(S,t,n,s)}};return Ae}function Xt(e){if(jt(e))return e=Ne(e),e.children=null,e}function ns(e){return jt(e)?e.children?e.children[0]:void 0:e}function fn(e,t){e.shapeFlag&6&&e.component?fn(e.component.subTree,t):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function qs(e,t=!1,n){let s=[],i=0;for(let r=0;r<e.length;r++){let l=e[r];const c=n==null?l.key:String(n)+String(l.key!=null?l.key:r);l.type===we?(l.patchFlag&128&&i++,s=s.concat(qs(l.children,t,c))):(t||l.type!==Ie)&&s.push(c!=null?Ne(l,{key:c}):l)}if(i>1)for(let r=0;r<s.length;r++)s[r].patchFlag=-2;return s}function Vs(e){return F(e)?{setup:e,name:e.name}:e}const It=e=>!!e.type.__asyncLoader,jt=e=>e.type.__isKeepAlive;function Fr(e,t){Js(e,"a",t)}function Sr(e,t){Js(e,"da",t)}function Js(e,t,n=V){const s=e.__wdc||(e.__wdc=()=>{let i=n;for(;i;){if(i.isDeactivated)return;i=i.parent}return e()});if(Ut(t,s,n),n){let i=n.parent;for(;i&&i.parent;)jt(i.parent.vnode)&&Rr(s,t,n,i),i=i.parent}}function Rr(e,t,n,s){const i=Ut(t,e,s,!0);Xs(()=>{wn(s[t],i)},n)}function Ut(e,t,n=V,s=!1){if(n){const i=n[e]||(n[e]=[]),r=t.__weh||(t.__weh=(...l)=>{if(n.isUnmounted)return;it(),nt(n);const c=fe(t,n,e,l);return Ve(),rt(),c});return s?i.unshift(r):i.push(r),r}}const Pe=e=>(t,n=V)=>(!mt||e==="sp")&&Ut(e,(...s)=>t(...s),n),Hr=Pe("bm"),Hn=Pe("m"),Br=Pe("bu"),Lr=Pe("u"),Ys=Pe("bum"),Xs=Pe("um"),Nr=Pe("sp"),Dr=Pe("rtg"),jr=Pe("rtc");function Ur(e,t=V){Ut("ec",e,t)}function Ue(e,t,n,s){const i=e.dirs,r=t&&t.dirs;for(let l=0;l<i.length;l++){const c=i[l];r&&(c.oldValue=r[l].value);let a=c.dir[s];a&&(it(),fe(a,n,8,[e.el,c,e,t]),rt())}}const $r=Symbol(),an=e=>e?fi(e)?Dn(e)||e.proxy:an(e.parent):null,at=G(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=>an(e.parent),$root:e=>an(e.root),$emit:e=>e.emit,$options:e=>Bn(e),$forceUpdate:e=>e.f||(e.f=()=>Rn(e.update)),$nextTick:e=>e.n||(e.n=dr.bind(e.proxy)),$watch:e=>Ir.bind(e)}),Zt=(e,t)=>e!==$&&!e.__isScriptSetup&&H(e,t),Kr={get({_:e},t){const{ctx:n,setupState:s,data:i,props:r,accessCache:l,type:c,appContext:a}=e;let h;if(t[0]!=="$"){const O=l[t];if(O!==void 0)switch(O){case 1:return s[t];case 2:return i[t];case 4:return n[t];case 3:return r[t]}else{if(Zt(s,t))return l[t]=1,s[t];if(i!==$&&H(i,t))return l[t]=2,i[t];if((h=e.propsOptions[0])&&H(h,t))return l[t]=3,r[t];if(n!==$&&H(n,t))return l[t]=4,n[t];un&&(l[t]=0)}}const g=at[t];let w,v;if(g)return t==="$attrs"&&ie(e,"get",t),g(e);if((w=c.__cssModules)&&(w=w[t]))return w;if(n!==$&&H(n,t))return l[t]=4,n[t];if(v=a.config.globalProperties,H(v,t))return v[t]},set({_:e},t,n){const{data:s,setupState:i,ctx:r}=e;return Zt(i,t)?(i[t]=n,!0):s!==$&&H(s,t)?(s[t]=n,!0):H(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(r[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:i,propsOptions:r}},l){let c;return!!n[l]||e!==$&&H(e,l)||Zt(t,l)||(c=r[0])&&H(c,l)||H(s,l)||H(at,l)||H(i.config.globalProperties,l)},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:H(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};let un=!0;function zr(e){const t=Bn(e),n=e.proxy,s=e.ctx;un=!1,t.beforeCreate&&ss(t.beforeCreate,e,"bc");const{data:i,computed:r,methods:l,watch:c,provide:a,inject:h,created:g,beforeMount:w,mounted:v,beforeUpdate:O,updated:L,activated:M,deactivated:k,beforeDestroy:D,beforeUnmount:oe,destroyed:P,unmounted:K,render:Z,renderTracked:Je,renderTriggered:Ae,errorCaptured:S,serverPrefetch:Y,expose:z,inheritAttrs:re,components:me,directives:Ce,filters:lt}=t;if(h&&Wr(h,s,null,e.appContext.config.unwrapInjectedRef),l)for(const W in l){const j=l[W];F(j)&&(s[W]=j.bind(n))}if(i){const W=i.call(n,n);J(W)&&(e.data=On(W))}if(un=!0,r)for(const W in r){const j=r[W],De=F(j)?j.bind(n,n):F(j.get)?j.get.bind(n,n):ge,_t=!F(j)&&F(j.set)?j.set.bind(n):ge,je=vl({get:De,set:_t});Object.defineProperty(s,W,{enumerable:!0,configurable:!0,get:()=>je.value,set:_e=>je.value=_e})}if(c)for(const W in c)Zs(c[W],s,n,W);if(a){const W=F(a)?a.call(n):a;Reflect.ownKeys(W).forEach(j=>{Mr(j,W[j])})}g&&ss(g,e,"c");function ee(W,j){A(j)?j.forEach(De=>W(De.bind(n))):j&&W(j.bind(n))}if(ee(Hr,w),ee(Hn,v),ee(Br,O),ee(Lr,L),ee(Fr,M),ee(Sr,k),ee(Ur,S),ee(jr,Je),ee(Dr,Ae),ee(Ys,oe),ee(Xs,K),ee(Nr,Y),A(z))if(z.length){const W=e.exposed||(e.exposed={});z.forEach(j=>{Object.defineProperty(W,j,{get:()=>n[j],set:De=>n[j]=De})})}else e.exposed||(e.exposed={});Z&&e.render===ge&&(e.render=Z),re!=null&&(e.inheritAttrs=re),me&&(e.components=me),Ce&&(e.directives=Ce)}function Wr(e,t,n=ge,s=!1){A(e)&&(e=hn(e));for(const i in e){const r=e[i];let l;J(r)?"default"in r?l=Mt(r.from||i,r.default,!0):l=Mt(r.from||i):l=Mt(r),ne(l)&&s?Object.defineProperty(t,i,{enumerable:!0,configurable:!0,get:()=>l.value,set:c=>l.value=c}):t[i]=l}}function ss(e,t,n){fe(A(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function Zs(e,t,n,s){const i=s.includes(".")?Ws(n,s):()=>n[s];if(X(e)){const r=t[e];F(r)&&Yt(i,r)}else if(F(e))Yt(i,e.bind(n));else if(J(e))if(A(e))e.forEach(r=>Zs(r,t,n,s));else{const r=F(e.handler)?e.handler.bind(n):t[e.handler];F(r)&&Yt(i,r,e)}}function Bn(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:i,optionsCache:r,config:{optionMergeStrategies:l}}=e.appContext,c=r.get(t);let a;return c?a=c:!i.length&&!n&&!s?a=t:(a={},i.length&&i.forEach(h=>St(a,h,l,!0)),St(a,t,l)),J(t)&&r.set(t,a),a}function St(e,t,n,s=!1){const{mixins:i,extends:r}=t;r&&St(e,r,n,!0),i&&i.forEach(l=>St(e,l,n,!0));for(const l in t)if(!(s&&l==="expose")){const c=kr[l]||n&&n[l];e[l]=c?c(e[l],t[l]):t[l]}return e}const kr={data:is,props:Ke,emits:Ke,methods:Ke,computed:Ke,beforeCreate:te,created:te,beforeMount:te,mounted:te,beforeUpdate:te,updated:te,beforeDestroy:te,beforeUnmount:te,destroyed:te,unmounted:te,activated:te,deactivated:te,errorCaptured:te,serverPrefetch:te,components:Ke,directives:Ke,watch:Vr,provide:is,inject:qr};function is(e,t){return t?e?function(){return G(F(e)?e.call(this,this):e,F(t)?t.call(this,this):t)}:t:e}function qr(e,t){return Ke(hn(e),hn(t))}function hn(e){if(A(e)){const t={};for(let n=0;n<e.length;n++)t[e[n]]=e[n];return t}return e}function te(e,t){return e?[...new Set([].concat(e,t))]:t}function Ke(e,t){return e?G(G(Object.create(null),e),t):t}function Vr(e,t){if(!e)return t;if(!t)return e;const n=G(Object.create(null),e);for(const s in t)n[s]=te(e[s],t[s]);return n}function Jr(e,t,n,s=!1){const i={},r={};At(r,Kt,1),e.propsDefaults=Object.create(null),Qs(e,t,i,r);for(const l in e.propsOptions[0])l in i||(i[l]=void 0);n?e.props=s?i:rr(i):e.type.props?e.props=i:e.props=r,e.attrs=r}function Yr(e,t,n,s){const{props:i,attrs:r,vnode:{patchFlag:l}}=e,c=B(i),[a]=e.propsOptions;let h=!1;if((s||l>0)&&!(l&16)){if(l&8){const g=e.vnode.dynamicProps;for(let w=0;w<g.length;w++){let v=g[w];if(Dt(e.emitsOptions,v))continue;const O=t[v];if(a)if(H(r,v))O!==r[v]&&(r[v]=O,h=!0);else{const L=tt(v);i[L]=dn(a,c,L,O,e,!1)}else O!==r[v]&&(r[v]=O,h=!0)}}}else{Qs(e,t,i,r)&&(h=!0);let g;for(const w in c)(!t||!H(t,w)&&((g=st(w))===w||!H(t,g)))&&(a?n&&(n[w]!==void 0||n[g]!==void 0)&&(i[w]=dn(a,c,w,void 0,e,!0)):delete i[w]);if(r!==c)for(const w in r)(!t||!H(t,w))&&(delete r[w],h=!0)}h&&Oe(e,"set","$attrs")}function Qs(e,t,n,s){const[i,r]=e.propsOptions;let l=!1,c;if(t)for(let a in t){if(Tt(a))continue;const h=t[a];let g;i&&H(i,g=tt(a))?!r||!r.includes(g)?n[g]=h:(c||(c={}))[g]=h:Dt(e.emitsOptions,a)||(!(a in s)||h!==s[a])&&(s[a]=h,l=!0)}if(r){const a=B(n),h=c||$;for(let g=0;g<r.length;g++){const w=r[g];n[w]=dn(i,a,w,h[w],e,!H(h,w))}}return l}function dn(e,t,n,s,i,r){const l=e[n];if(l!=null){const c=H(l,"default");if(c&&s===void 0){const a=l.default;if(l.type!==Function&&F(a)){const{propsDefaults:h}=i;n in h?s=h[n]:(nt(i),s=h[n]=a.call(null,t),Ve())}else s=a}l[0]&&(r&&!c?s=!1:l[1]&&(s===""||s===st(n))&&(s=!0))}return s}function Gs(e,t,n=!1){const s=t.propsCache,i=s.get(e);if(i)return i;const r=e.props,l={},c=[];let a=!1;if(!F(e)){const g=w=>{a=!0;const[v,O]=Gs(w,t,!0);G(l,v),O&&c.push(...O)};!n&&t.mixins.length&&t.mixins.forEach(g),e.extends&&g(e.extends),e.mixins&&e.mixins.forEach(g)}if(!r&&!a)return J(e)&&s.set(e,Qe),Qe;if(A(r))for(let g=0;g<r.length;g++){const w=tt(r[g]);rs(w)&&(l[w]=$)}else if(r)for(const g in r){const w=tt(g);if(rs(w)){const v=r[g],O=l[w]=A(v)||F(v)?{type:v}:Object.assign({},v);if(O){const L=cs(Boolean,O.type),M=cs(String,O.type);O[0]=L>-1,O[1]=M<0||L<M,(L>-1||H(O,"default"))&&c.push(w)}}}const h=[l,c];return J(e)&&s.set(e,h),h}function rs(e){return e[0]!=="$"}function ls(e){const t=e&&e.toString().match(/^\s*(function|class) (\w+)/);return t?t[2]:e===null?"null":""}function os(e,t){return ls(e)===ls(t)}function cs(e,t){return A(t)?t.findIndex(n=>os(n,e)):F(t)&&os(t,e)?0:-1}const ei=e=>e[0]==="_"||e==="$stable",Ln=e=>A(e)?e.map(ye):[ye(e)],Xr=(e,t,n)=>{if(t._n)return t;const s=xr((...i)=>Ln(t(...i)),n);return s._c=!1,s},ti=(e,t,n)=>{const s=e._ctx;for(const i in e){if(ei(i))continue;const r=e[i];if(F(r))t[i]=Xr(i,r,s);else if(r!=null){const l=Ln(r);t[i]=()=>l}}},ni=(e,t)=>{const n=Ln(t);e.slots.default=()=>n},Zr=(e,t)=>{if(e.vnode.shapeFlag&32){const n=t._;n?(e.slots=B(t),At(t,"_",n)):ti(t,e.slots={})}else e.slots={},t&&ni(e,t);At(e.slots,Kt,1)},Qr=(e,t,n)=>{const{vnode:s,slots:i}=e;let r=!0,l=$;if(s.shapeFlag&32){const c=t._;c?n&&c===1?r=!1:(G(i,t),!n&&c===1&&delete i._):(r=!t.$stable,ti(t,i)),l=t}else t&&(ni(e,t),l={default:1});if(r)for(const c in i)!ei(c)&&!(c in l)&&delete i[c]};function si(){return{app:null,config:{isNativeTag:xi,performance:!1,globalProperties:{},optionMergeStrategies:{},errorHandler:void 0,warnHandler:void 0,compilerOptions:{}},mixins:[],components:{},directives:{},provides:Object.create(null),optionsCache:new WeakMap,propsCache:new WeakMap,emitsCache:new WeakMap}}let Gr=0;function el(e,t){return function(s,i=null){F(s)||(s=Object.assign({},s)),i!=null&&!J(i)&&(i=null);const r=si(),l=new Set;let c=!1;const a=r.app={_uid:Gr++,_component:s,_props:i,_container:null,_context:r,_instance:null,version:Tl,get config(){return r.config},set config(h){},use(h,...g){return l.has(h)||(h&&F(h.install)?(l.add(h),h.install(a,...g)):F(h)&&(l.add(h),h(a,...g))),a},mixin(h){return r.mixins.includes(h)||r.mixins.push(h),a},component(h,g){return g?(r.components[h]=g,a):r.components[h]},directive(h,g){return g?(r.directives[h]=g,a):r.directives[h]},mount(h,g,w){if(!c){const v=Be(s,i);return v.appContext=r,g&&t?t(v,h):e(v,h,w),c=!0,a._container=h,h.__vue_app__=a,Dn(v.component)||v.component.proxy}},unmount(){c&&(e(null,a._container),delete a._container.__vue_app__)},provide(h,g){return r.provides[h]=g,a}};return a}}function pn(e,t,n,s,i=!1){if(A(e)){e.forEach((v,O)=>pn(v,t&&(A(t)?t[O]:t),n,s,i));return}if(It(s)&&!i)return;const r=s.shapeFlag&4?Dn(s.component)||s.component.proxy:s.el,l=i?null:r,{i:c,r:a}=e,h=t&&t.r,g=c.refs===$?c.refs={}:c.refs,w=c.setupState;if(h!=null&&h!==a&&(X(h)?(g[h]=null,H(w,h)&&(w[h]=null)):ne(h)&&(h.value=null)),F(a))He(a,c,12,[l,g]);else{const v=X(a),O=ne(a);if(v||O){const L=()=>{if(e.f){const M=v?H(w,a)?w[a]:g[a]:a.value;i?A(M)&&wn(M,r):A(M)?M.includes(r)||M.push(r):v?(g[a]=[r],H(w,a)&&(w[a]=g[a])):(a.value=[r],e.k&&(g[e.k]=a.value))}else v?(g[a]=l,H(w,a)&&(w[a]=l)):O&&(a.value=l,e.k&&(g[e.k]=l))};l?(L.id=-1,se(L,n)):L()}}}const se=Tr;function tl(e){return nl(e)}function nl(e,t){const n=Pi();n.__VUE__=!0;const{insert:s,remove:i,patchProp:r,createElement:l,createText:c,createComment:a,setText:h,setElementText:g,parentNode:w,nextSibling:v,setScopeId:O=ge,insertStaticContent:L}=e,M=(o,f,u,p=null,d=null,b=null,y=!1,_=null,x=!!f.dynamicChildren)=>{if(o===f)return;o&&!We(o,f)&&(p=bt(o),_e(o,d,b,!0),o=null),f.patchFlag===-2&&(x=!1,f.dynamicChildren=null);const{type:m,ref:E,shapeFlag:C}=f;switch(m){case $t:k(o,f,u,p);break;case Ie:D(o,f,u,p);break;case Qt:o==null&&oe(f,u,p,y);break;case we:me(o,f,u,p,d,b,y,_,x);break;default:C&1?Z(o,f,u,p,d,b,y,_,x):C&6?Ce(o,f,u,p,d,b,y,_,x):(C&64||C&128)&&m.process(o,f,u,p,d,b,y,_,x,Ye)}E!=null&&d&&pn(E,o&&o.ref,b,f||o,!f)},k=(o,f,u,p)=>{if(o==null)s(f.el=c(f.children),u,p);else{const d=f.el=o.el;f.children!==o.children&&h(d,f.children)}},D=(o,f,u,p)=>{o==null?s(f.el=a(f.children||""),u,p):f.el=o.el},oe=(o,f,u,p)=>{[o.el,o.anchor]=L(o.children,f,u,p,o.el,o.anchor)},P=({el:o,anchor:f},u,p)=>{let d;for(;o&&o!==f;)d=v(o),s(o,u,p),o=d;s(f,u,p)},K=({el:o,anchor:f})=>{let u;for(;o&&o!==f;)u=v(o),i(o),o=u;i(f)},Z=(o,f,u,p,d,b,y,_,x)=>{y=y||f.type==="svg",o==null?Je(f,u,p,d,b,y,_,x):Y(o,f,d,b,y,_,x)},Je=(o,f,u,p,d,b,y,_)=>{let x,m;const{type:E,props:C,shapeFlag:T,transition:I,dirs:R}=o;if(x=o.el=l(o.type,b,C&&C.is,C),T&8?g(x,o.children):T&16&&S(o.children,x,null,p,d,b&&E!=="foreignObject",y,_),R&&Ue(o,null,p,"created"),Ae(x,o,o.scopeId,y,p),C){for(const N in C)N!=="value"&&!Tt(N)&&r(x,N,null,C[N],b,o.children,p,d,Ee);"value"in C&&r(x,"value",null,C.value),(m=C.onVnodeBeforeMount)&&xe(m,p,o)}R&&Ue(o,null,p,"beforeMount");const U=(!d||d&&!d.pendingBranch)&&I&&!I.persisted;U&&I.beforeEnter(x),s(x,f,u),((m=C&&C.onVnodeMounted)||U||R)&&se(()=>{m&&xe(m,p,o),U&&I.enter(x),R&&Ue(o,null,p,"mounted")},d)},Ae=(o,f,u,p,d)=>{if(u&&O(o,u),p)for(let b=0;b<p.length;b++)O(o,p[b]);if(d){let b=d.subTree;if(f===b){const y=d.vnode;Ae(o,y,y.scopeId,y.slotScopeIds,d.parent)}}},S=(o,f,u,p,d,b,y,_,x=0)=>{for(let m=x;m<o.length;m++){const E=o[m]=_?Se(o[m]):ye(o[m]);M(null,E,f,u,p,d,b,y,_)}},Y=(o,f,u,p,d,b,y)=>{const _=f.el=o.el;let{patchFlag:x,dynamicChildren:m,dirs:E}=f;x|=o.patchFlag&16;const C=o.props||$,T=f.props||$;let I;u&&$e(u,!1),(I=T.onVnodeBeforeUpdate)&&xe(I,u,f,o),E&&Ue(f,o,u,"beforeUpdate"),u&&$e(u,!0);const R=d&&f.type!=="foreignObject";if(m?z(o.dynamicChildren,m,_,u,p,R,b):y||j(o,f,_,null,u,p,R,b,!1),x>0){if(x&16)re(_,f,C,T,u,p,d);else if(x&2&&C.class!==T.class&&r(_,"class",null,T.class,d),x&4&&r(_,"style",C.style,T.style,d),x&8){const U=f.dynamicProps;for(let N=0;N<U.length;N++){const q=U[N],ae=C[q],Xe=T[q];(Xe!==ae||q==="value")&&r(_,q,ae,Xe,d,o.children,u,p,Ee)}}x&1&&o.children!==f.children&&g(_,f.children)}else!y&&m==null&&re(_,f,C,T,u,p,d);((I=T.onVnodeUpdated)||E)&&se(()=>{I&&xe(I,u,f,o),E&&Ue(f,o,u,"updated")},p)},z=(o,f,u,p,d,b,y)=>{for(let _=0;_<f.length;_++){const x=o[_],m=f[_],E=x.el&&(x.type===we||!We(x,m)||x.shapeFlag&70)?w(x.el):u;M(x,m,E,null,p,d,b,y,!0)}},re=(o,f,u,p,d,b,y)=>{if(u!==p){if(u!==$)for(const _ in u)!Tt(_)&&!(_ in p)&&r(o,_,u[_],null,y,f.children,d,b,Ee);for(const _ in p){if(Tt(_))continue;const x=p[_],m=u[_];x!==m&&_!=="value"&&r(o,_,m,x,y,f.children,d,b,Ee)}"value"in p&&r(o,"value",u.value,p.value)}},me=(o,f,u,p,d,b,y,_,x)=>{const m=f.el=o?o.el:c(""),E=f.anchor=o?o.anchor:c("");let{patchFlag:C,dynamicChildren:T,slotScopeIds:I}=f;I&&(_=_?_.concat(I):I),o==null?(s(m,u,p),s(E,u,p),S(f.children,u,E,d,b,y,_,x)):C>0&&C&64&&T&&o.dynamicChildren?(z(o.dynamicChildren,T,u,d,b,y,_),(f.key!=null||d&&f===d.subTree)&&ii(o,f,!0)):j(o,f,u,E,d,b,y,_,x)},Ce=(o,f,u,p,d,b,y,_,x)=>{f.slotScopeIds=_,o==null?f.shapeFlag&512?d.ctx.activate(f,u,p,y,x):lt(f,u,p,d,b,y,x):Un(o,f,x)},lt=(o,f,u,p,d,b,y)=>{const _=o.component=gl(o,p,d);if(jt(o)&&(_.ctx.renderer=Ye),_l(_),_.asyncDep){if(d&&d.registerDep(_,ee),!o.el){const x=_.subTree=Be(Ie);D(null,x,f,u)}return}ee(_,o,f,u,d,b,y)},Un=(o,f,u)=>{const p=f.component=o.component;if(vr(o,f,u))if(p.asyncDep&&!p.asyncResolved){W(p,f,u);return}else p.next=f,gr(p.update),p.update();else f.el=o.el,p.vnode=f},ee=(o,f,u,p,d,b,y)=>{const _=()=>{if(o.isMounted){let{next:E,bu:C,u:T,parent:I,vnode:R}=o,U=E,N;$e(o,!1),E?(E.el=R.el,W(o,E,y)):E=R,C&&Vt(C),(N=E.props&&E.props.onVnodeBeforeUpdate)&&xe(N,I,E,R),$e(o,!0);const q=Jt(o),ae=o.subTree;o.subTree=q,M(ae,q,w(ae.el),bt(ae),o,d,b),E.el=q.el,U===null&&Cr(o,q.el),T&&se(T,d),(N=E.props&&E.props.onVnodeUpdated)&&se(()=>xe(N,I,E,R),d)}else{let E;const{el:C,props:T}=f,{bm:I,m:R,parent:U}=o,N=It(f);if($e(o,!1),I&&Vt(I),!N&&(E=T&&T.onVnodeBeforeMount)&&xe(E,U,f),$e(o,!0),C&&kt){const q=()=>{o.subTree=Jt(o),kt(C,o.subTree,o,d,null)};N?f.type.__asyncLoader().then(()=>!o.isUnmounted&&q()):q()}else{const q=o.subTree=Jt(o);M(null,q,u,p,o,d,b),f.el=q.el}if(R&&se(R,d),!N&&(E=T&&T.onVnodeMounted)){const q=f;se(()=>xe(E,U,q),d)}(f.shapeFlag&256||U&&It(U.vnode)&&U.vnode.shapeFlag&256)&&o.a&&se(o.a,d),o.isMounted=!0,f=u=p=null}},x=o.effect=new En(_,()=>Rn(m),o.scope),m=o.update=()=>x.run();m.id=o.uid,$e(o,!0),m()},W=(o,f,u)=>{f.component=o;const p=o.vnode.props;o.vnode=f,o.next=null,Yr(o,f.props,p,u),Qr(o,f.children,u),it(),es(),rt()},j=(o,f,u,p,d,b,y,_,x=!1)=>{const m=o&&o.children,E=o?o.shapeFlag:0,C=f.children,{patchFlag:T,shapeFlag:I}=f;if(T>0){if(T&128){_t(m,C,u,p,d,b,y,_,x);return}else if(T&256){De(m,C,u,p,d,b,y,_,x);return}}I&8?(E&16&&Ee(m,d,b),C!==m&&g(u,C)):E&16?I&16?_t(m,C,u,p,d,b,y,_,x):Ee(m,d,b,!0):(E&8&&g(u,""),I&16&&S(C,u,p,d,b,y,_,x))},De=(o,f,u,p,d,b,y,_,x)=>{o=o||Qe,f=f||Qe;const m=o.length,E=f.length,C=Math.min(m,E);let T;for(T=0;T<C;T++){const I=f[T]=x?Se(f[T]):ye(f[T]);M(o[T],I,u,null,d,b,y,_,x)}m>E?Ee(o,d,b,!0,!1,C):S(f,u,p,d,b,y,_,x,C)},_t=(o,f,u,p,d,b,y,_,x)=>{let m=0;const E=f.length;let C=o.length-1,T=E-1;for(;m<=C&&m<=T;){const I=o[m],R=f[m]=x?Se(f[m]):ye(f[m]);if(We(I,R))M(I,R,u,null,d,b,y,_,x);else break;m++}for(;m<=C&&m<=T;){const I=o[C],R=f[T]=x?Se(f[T]):ye(f[T]);if(We(I,R))M(I,R,u,null,d,b,y,_,x);else break;C--,T--}if(m>C){if(m<=T){const I=T+1,R=I<E?f[I].el:p;for(;m<=T;)M(null,f[m]=x?Se(f[m]):ye(f[m]),u,R,d,b,y,_,x),m++}}else if(m>T)for(;m<=C;)_e(o[m],d,b,!0),m++;else{const I=m,R=m,U=new Map;for(m=R;m<=T;m++){const le=f[m]=x?Se(f[m]):ye(f[m]);le.key!=null&&U.set(le.key,m)}let N,q=0;const ae=T-R+1;let Xe=!1,zn=0;const ot=new Array(ae);for(m=0;m<ae;m++)ot[m]=0;for(m=I;m<=C;m++){const le=o[m];if(q>=ae){_e(le,d,b,!0);continue}let be;if(le.key!=null)be=U.get(le.key);else for(N=R;N<=T;N++)if(ot[N-R]===0&&We(le,f[N])){be=N;break}be===void 0?_e(le,d,b,!0):(ot[be-R]=m+1,be>=zn?zn=be:Xe=!0,M(le,f[be],u,null,d,b,y,_,x),q++)}const Wn=Xe?sl(ot):Qe;for(N=Wn.length-1,m=ae-1;m>=0;m--){const le=R+m,be=f[le],kn=le+1<E?f[le+1].el:p;ot[m]===0?M(null,be,u,kn,d,b,y,_,x):Xe&&(N<0||m!==Wn[N]?je(be,u,kn,2):N--)}}},je=(o,f,u,p,d=null)=>{const{el:b,type:y,transition:_,children:x,shapeFlag:m}=o;if(m&6){je(o.component.subTree,f,u,p);return}if(m&128){o.suspense.move(f,u,p);return}if(m&64){y.move(o,f,u,Ye);return}if(y===we){s(b,f,u);for(let C=0;C<x.length;C++)je(x[C],f,u,p);s(o.anchor,f,u);return}if(y===Qt){P(o,f,u);return}if(p!==2&&m&1&&_)if(p===0)_.beforeEnter(b),s(b,f,u),se(()=>_.enter(b),d);else{const{leave:C,delayLeave:T,afterLeave:I}=_,R=()=>s(b,f,u),U=()=>{C(b,()=>{R(),I&&I()})};T?T(b,R,U):U()}else s(b,f,u)},_e=(o,f,u,p=!1,d=!1)=>{const{type:b,props:y,ref:_,children:x,dynamicChildren:m,shapeFlag:E,patchFlag:C,dirs:T}=o;if(_!=null&&pn(_,null,u,o,!0),E&256){f.ctx.deactivate(o);return}const I=E&1&&T,R=!It(o);let U;if(R&&(U=y&&y.onVnodeBeforeUnmount)&&xe(U,f,o),E&6)hi(o.component,u,p);else{if(E&128){o.suspense.unmount(u,p);return}I&&Ue(o,null,f,"beforeUnmount"),E&64?o.type.remove(o,f,u,d,Ye,p):m&&(b!==we||C>0&&C&64)?Ee(m,f,u,!1,!0):(b===we&&C&384||!d&&E&16)&&Ee(x,f,u),p&&$n(o)}(R&&(U=y&&y.onVnodeUnmounted)||I)&&se(()=>{U&&xe(U,f,o),I&&Ue(o,null,f,"unmounted")},u)},$n=o=>{const{type:f,el:u,anchor:p,transition:d}=o;if(f===we){ui(u,p);return}if(f===Qt){K(o);return}const b=()=>{i(u),d&&!d.persisted&&d.afterLeave&&d.afterLeave()};if(o.shapeFlag&1&&d&&!d.persisted){const{leave:y,delayLeave:_}=d,x=()=>y(u,b);_?_(o.el,b,x):x()}else b()},ui=(o,f)=>{let u;for(;o!==f;)u=v(o),i(o),o=u;i(f)},hi=(o,f,u)=>{const{bum:p,scope:d,update:b,subTree:y,um:_}=o;p&&Vt(p),d.stop(),b&&(b.active=!1,_e(y,o,f,u)),_&&se(_,f),se(()=>{o.isUnmounted=!0},f),f&&f.pendingBranch&&!f.isUnmounted&&o.asyncDep&&!o.asyncResolved&&o.suspenseId===f.pendingId&&(f.deps--,f.deps===0&&f.resolve())},Ee=(o,f,u,p=!1,d=!1,b=0)=>{for(let y=b;y<o.length;y++)_e(o[y],f,u,p,d)},bt=o=>o.shapeFlag&6?bt(o.component.subTree):o.shapeFlag&128?o.suspense.next():v(o.anchor||o.el),Kn=(o,f,u)=>{o==null?f._vnode&&_e(f._vnode,null,null,!0):M(f._vnode||null,o,f,null,null,null,u),es(),js(),f._vnode=o},Ye={p:M,um:_e,m:je,r:$n,mt:lt,mc:S,pc:j,pbc:z,n:bt,o:e};let Wt,kt;return t&&([Wt,kt]=t(Ye)),{render:Kn,hydrate:Wt,createApp:el(Kn,Wt)}}function $e({effect:e,update:t},n){e.allowRecurse=t.allowRecurse=n}function ii(e,t,n=!1){const s=e.children,i=t.children;if(A(s)&&A(i))for(let r=0;r<s.length;r++){const l=s[r];let c=i[r];c.shapeFlag&1&&!c.dynamicChildren&&((c.patchFlag<=0||c.patchFlag===32)&&(c=i[r]=Se(i[r]),c.el=l.el),n||ii(l,c)),c.type===$t&&(c.el=l.el)}}function sl(e){const t=e.slice(),n=[0];let s,i,r,l,c;const a=e.length;for(s=0;s<a;s++){const h=e[s];if(h!==0){if(i=n[n.length-1],e[i]<h){t[s]=i,n.push(s);continue}for(r=0,l=n.length-1;r<l;)c=r+l>>1,e[n[c]]<h?r=c+1:l=c;h<e[n[r]]&&(r>0&&(t[s]=n[r-1]),n[r]=s)}}for(r=n.length,l=n[r-1];r-- >0;)n[r]=l,l=t[l];return n}const il=e=>e.__isTeleport,we=Symbol(void 0),$t=Symbol(void 0),Ie=Symbol(void 0),Qt=Symbol(void 0),ut=[];let pe=null;function ri(e=!1){ut.push(pe=e?null:[])}function rl(){ut.pop(),pe=ut[ut.length-1]||null}let gt=1;function fs(e){gt+=e}function li(e){return e.dynamicChildren=gt>0?pe||Qe:null,rl(),gt>0&&pe&&pe.push(e),e}function ll(e,t,n,s,i,r){return li(ci(e,t,n,s,i,r,!0))}function ol(e,t,n,s,i){return li(Be(e,t,n,s,i,!0))}function cl(e){return e?e.__v_isVNode===!0:!1}function We(e,t){return e.type===t.type&&e.key===t.key}const Kt="__vInternal",oi=({key:e})=>e??null,Ot=({ref:e,ref_key:t,ref_for:n})=>e!=null?X(e)||ne(e)||F(e)?{i:de,r:e,k:t,f:!!n}:e:null;function ci(e,t=null,n=null,s=0,i=null,r=e===we?0:1,l=!1,c=!1){const a={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&oi(t),ref:t&&Ot(t),scopeId:Ks,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetAnchor:null,staticCount:0,shapeFlag:r,patchFlag:s,dynamicProps:i,dynamicChildren:null,appContext:null,ctx:de};return c?(Nn(a,n),r&128&&e.normalize(a)):n&&(a.shapeFlag|=X(n)?8:16),gt>0&&!l&&pe&&(a.patchFlag>0||r&6)&&a.patchFlag!==32&&pe.push(a),a}const Be=fl;function fl(e,t=null,n=null,s=0,i=null,r=!1){if((!e||e===$r)&&(e=Ie),cl(e)){const c=Ne(e,t,!0);return n&&Nn(c,n),gt>0&&!r&&pe&&(c.shapeFlag&6?pe[pe.indexOf(e)]=c:pe.push(c)),c.patchFlag|=-2,c}if(yl(e)&&(e=e.__vccOpts),t){t=al(t);let{class:c,style:a}=t;c&&!X(c)&&(t.class=bn(c)),J(a)&&(Rs(a)&&!A(a)&&(a=G({},a)),t.style=_n(a))}const l=X(e)?1:Er(e)?128:il(e)?64:J(e)?4:F(e)?2:0;return ci(e,t,n,s,i,l,r,!0)}function al(e){return e?Rs(e)||Kt in e?G({},e):e:null}function Ne(e,t,n=!1){const{props:s,ref:i,patchFlag:r,children:l}=e,c=t?hl(s||{},t):s;return{__v_isVNode:!0,__v_skip:!0,type:e.type,props:c,key:c&&oi(c),ref:t&&t.ref?n&&i?A(i)?i.concat(Ot(t)):[i,Ot(t)]:Ot(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==we?r===-1?16:r|16:r,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:e.transition,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&Ne(e.ssContent),ssFallback:e.ssFallback&&Ne(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce}}function ul(e=" ",t=0){return Be($t,null,e,t)}function ye(e){return e==null||typeof e=="boolean"?Be(Ie):A(e)?Be(we,null,e.slice()):typeof e=="object"?Se(e):Be($t,null,String(e))}function Se(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:Ne(e)}function Nn(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(A(t))n=16;else if(typeof t=="object")if(s&65){const i=t.default;i&&(i._c&&(i._d=!1),Nn(e,i()),i._c&&(i._d=!0));return}else{n=32;const i=t._;!i&&!(Kt in t)?t._ctx=de:i===3&&de&&(de.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else F(t)?(t={default:t,_ctx:de},n=32):(t=String(t),s&64?(n=16,t=[ul(t)]):n=8);e.children=t,e.shapeFlag|=n}function hl(...e){const t={};for(let n=0;n<e.length;n++){const s=e[n];for(const i in s)if(i==="class")t.class!==s.class&&(t.class=bn([t.class,s.class]));else if(i==="style")t.style=_n([t.style,s.style]);else if(Rt(i)){const r=t[i],l=s[i];l&&r!==l&&!(A(r)&&r.includes(l))&&(t[i]=r?[].concat(r,l):l)}else i!==""&&(t[i]=s[i])}return t}function xe(e,t,n,s=null){fe(e,t,7,[n,s])}const dl=si();let pl=0;function gl(e,t,n){const s=e.type,i=(t?t.appContext:e.appContext)||dl,r={uid:pl++,vnode:e,type:s,parent:t,appContext:i,root:null,next:null,subTree:null,effect:null,update:null,scope:new Ai(!0),render:null,proxy:null,exposed:null,exposeProxy:null,withProxy:null,provides:t?t.provides:Object.create(i.provides),accessCache:null,renderCache:[],components:null,directives:null,propsOptions:Gs(s,i),emitsOptions:$s(s,i),emit:null,emitted:null,propsDefaults:$,inheritAttrs:s.inheritAttrs,ctx:$,data:$,props:$,attrs:$,slots:$,refs:$,setupState:$,setupContext:null,suspense:n,suspenseId:n?n.pendingId:0,asyncDep:null,asyncResolved:!1,isMounted:!1,isUnmounted:!1,isDeactivated:!1,bc:null,c:null,bm:null,m:null,bu:null,u:null,um:null,bum:null,da:null,a:null,rtg:null,rtc:null,ec:null,sp:null};return r.ctx={_:r},r.root=t?t.root:r,r.emit=br.bind(null,r),e.ce&&e.ce(r),r}let V=null;const ml=()=>V||de,nt=e=>{V=e,e.scope.on()},Ve=()=>{V&&V.scope.off(),V=null};function fi(e){return e.vnode.shapeFlag&4}let mt=!1;function _l(e,t=!1){mt=t;const{props:n,children:s}=e.vnode,i=fi(e);Jr(e,n,i,t),Zr(e,s);const r=i?bl(e,t):void 0;return mt=!1,r}function bl(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=Hs(new Proxy(e.ctx,Kr));const{setup:s}=n;if(s){const i=e.setupContext=s.length>1?wl(e):null;nt(e),it();const r=He(s,e,0,[e.props,i]);if(rt(),Ve(),ws(r)){if(r.then(Ve,Ve),t)return r.then(l=>{as(e,l,t)}).catch(l=>{Nt(l,e,0)});e.asyncDep=r}else as(e,r,t)}else ai(e,t)}function as(e,t,n){F(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:J(t)&&(e.setupState=Bs(t)),ai(e,n)}let us;function ai(e,t,n){const s=e.type;if(!e.render){if(!t&&us&&!s.render){const i=s.template||Bn(e).template;if(i){const{isCustomElement:r,compilerOptions:l}=e.appContext.config,{delimiters:c,compilerOptions:a}=s,h=G(G({isCustomElement:r,delimiters:c},l),a);s.render=us(i,h)}}e.render=s.render||ge}nt(e),it(),zr(e),rt(),Ve()}function xl(e){return new Proxy(e.attrs,{get(t,n){return ie(e,"get","$attrs"),t[n]}})}function wl(e){const t=s=>{e.exposed=s||{}};let n;return{get attrs(){return n||(n=xl(e))},slots:e.slots,emit:e.emit,expose:t}}function Dn(e){if(e.exposed)return e.exposeProxy||(e.exposeProxy=new Proxy(Bs(Hs(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in at)return at[n](e)},has(t,n){return n in t||n in at}}))}function yl(e){return F(e)&&"__vccOpts"in e}const vl=(e,t)=>ur(e,t,mt),Cl=Symbol(""),El=()=>Mt(Cl),Tl="3.2.47",Ml="http://www.w3.org/2000/svg",ke=typeof document<"u"?document:null,hs=ke&&ke.createElement("template"),Il={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const i=t?ke.createElementNS(Ml,e):ke.createElement(e,n?{is:n}:void 0);return e==="select"&&s&&s.multiple!=null&&i.setAttribute("multiple",s.multiple),i},createText:e=>ke.createTextNode(e),createComment:e=>ke.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>ke.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,i,r){const l=n?n.previousSibling:t.lastChild;if(i&&(i===r||i.nextSibling))for(;t.insertBefore(i.cloneNode(!0),n),!(i===r||!(i=i.nextSibling)););else{hs.innerHTML=s?`<svg>${e}</svg>`:e;const c=hs.content;if(s){const a=c.firstChild;for(;a.firstChild;)c.appendChild(a.firstChild);c.removeChild(a)}t.insertBefore(c,n)}return[l?l.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}};function Ol(e,t,n){const s=e._vtc;s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}function Pl(e,t,n){const s=e.style,i=X(n);if(n&&!i){if(t&&!X(t))for(const r in t)n[r]==null&&gn(s,r,"");for(const r in n)gn(s,r,n[r])}else{const r=s.display;i?t!==n&&(s.cssText=n):t&&e.removeAttribute("style"),"_vod"in e&&(s.display=r)}}const ds=/\s*!important$/;function gn(e,t,n){if(A(n))n.forEach(s=>gn(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=Al(e,t);ds.test(n)?e.setProperty(st(s),n.replace(ds,""),"important"):e[s]=n}}const ps=["Webkit","Moz","ms"],Gt={};function Al(e,t){const n=Gt[t];if(n)return n;let s=tt(t);if(s!=="filter"&&s in e)return Gt[t]=s;s=ys(s);for(let i=0;i<ps.length;i++){const r=ps[i]+s;if(r in e)return Gt[t]=r}return t}const gs="http://www.w3.org/1999/xlink";function Fl(e,t,n,s,i){if(s&&t.startsWith("xlink:"))n==null?e.removeAttributeNS(gs,t.slice(6,t.length)):e.setAttributeNS(gs,t,n);else{const r=bi(t);n==null||r&&!xs(n)?e.removeAttribute(t):e.setAttribute(t,r?"":n)}}function Sl(e,t,n,s,i,r,l){if(t==="innerHTML"||t==="textContent"){s&&l(s,i,r),e[t]=n??"";return}if(t==="value"&&e.tagName!=="PROGRESS"&&!e.tagName.includes("-")){e._value=n;const a=n??"";(e.value!==a||e.tagName==="OPTION")&&(e.value=a),n==null&&e.removeAttribute(t);return}let c=!1;if(n===""||n==null){const a=typeof e[t];a==="boolean"?n=xs(n):n==null&&a==="string"?(n="",c=!0):a==="number"&&(n=0,c=!0)}try{e[t]=n}catch{}c&&e.removeAttribute(t)}function Rl(e,t,n,s){e.addEventListener(t,n,s)}function Hl(e,t,n,s){e.removeEventListener(t,n,s)}function Bl(e,t,n,s,i=null){const r=e._vei||(e._vei={}),l=r[t];if(s&&l)l.value=s;else{const[c,a]=Ll(t);if(s){const h=r[t]=jl(s,i);Rl(e,c,h,a)}else l&&(Hl(e,c,l,a),r[t]=void 0)}}const ms=/(?:Once|Passive|Capture)$/;function Ll(e){let t;if(ms.test(e)){t={};let s;for(;s=e.match(ms);)e=e.slice(0,e.length-s[0].length),t[s[0].toLowerCase()]=!0}return[e[2]===":"?e.slice(3):st(e.slice(2)),t]}let en=0;const Nl=Promise.resolve(),Dl=()=>en||(Nl.then(()=>en=0),en=Date.now());function jl(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;fe(Ul(s,n.value),t,5,[s])};return n.value=e,n.attached=Dl(),n}function Ul(e,t){if(A(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(s=>i=>!i._stopped&&s&&s(i))}else return t}const _s=/^on[a-z]/,$l=(e,t,n,s,i=!1,r,l,c,a)=>{t==="class"?Ol(e,s,i):t==="style"?Pl(e,n,s):Rt(t)?xn(t)||Bl(e,t,n,s,l):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):Kl(e,t,s,i))?Sl(e,t,s,r,l,c,a):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),Fl(e,t,s,i))};function Kl(e,t,n,s){return s?!!(t==="innerHTML"||t==="textContent"||t in e&&_s.test(t)&&F(n)):t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA"||_s.test(t)&&X(n)?!1:t in e}const zl={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String};Ar.props;const Wl=G({patchProp:$l},Il);let bs;function kl(){return bs||(bs=tl(Wl))}const ql=(...e)=>{const t=kl().createApp(...e),{mount:n}=t;return t.mount=s=>{const i=Vl(s);if(!i)return;const r=t._component;!F(r)&&!r.render&&!r.template&&(r.template=i.innerHTML),i.innerHTML="";const l=n(i,!1,i instanceof SVGElement);return i instanceof Element&&(i.removeAttribute("v-cloak"),i.setAttribute("data-v-app","")),l},t};function Vl(e){return X(e)?document.querySelector(e):e}const Te=(e=1,t=e+1,n=!1)=>{const s=parseFloat(e),i=parseFloat(t),r=Math.random()*(i-s)+s;return n?Math.round(r):r};class zt{constructor({color:t="blue",size:n=10,dropRate:s=10}={}){this.color=t,this.size=n,this.dropRate=s}setup({canvas:t,wind:n,windPosCoef:s,windSpeedMax:i,count:r}){return this.canvas=t,this.wind=n,this.windPosCoef=s,this.windSpeedMax=i,this.x=Te(-35,this.canvas.width+35),this.y=Te(-30,-35),this.d=Te(150)+10,this.particleSize=Te(this.size,this.size*2),this.tilt=Te(10),this.tiltAngleIncremental=(Te(0,.08)+.04)*(Te()<.5?-1:1),this.tiltAngle=0,this.angle=Te(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 Jl extends zt{draw(){super.draw(),this.canvas.ctx.arc(0,0,this.particleSize/2,0,Math.PI*2,!1),this.canvas.ctx.fill()}}class Yl extends zt{draw(){super.draw(),this.canvas.ctx.fillRect(0,0,this.particleSize,this.particleSize/2)}}class Xl extends zt{draw(){super.draw();const t=(n,s,i,r,l,c)=>{this.canvas.ctx.bezierCurveTo(n*(this.particleSize/200),s*(this.particleSize/200),i*(this.particleSize/200),r*(this.particleSize/200),l*(this.particleSize/200),c*(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 Zl extends zt{constructor(t,n){super(t),this.imgEl=n}draw(){super.draw(),this.canvas.ctx.drawImage(this.imgEl,0,0,this.particleSize,this.particleSize)}}class Ql{constructor(){this.cachedImages={}}createImageElement(t){const n=document.createElement("img");return n.setAttribute("src",t),n}getImageElement(t){return this.cachedImages[t]||(this.cachedImages[t]=this.createImageElement(t)),this.cachedImages[t]}getRandomParticle(t={}){const n=t.particles||[];return n.length<1?{}:n[Math.floor(Math.random()*n.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 n=this.getDefaults(t),s=this.getRandomParticle(t),i=Object.assign(n,s),r=Te(0,i.colors.length-1,!0);if(i.color=i.colors[r],i.type==="circle")return new Jl(i);if(i.type==="rect")return new Yl(i);if(i.type==="heart")return new Xl(i);if(i.type==="image")return new Zl(i,this.getImageElement(i.url));throw Error(`Unknown particle type: "${i.type}"`)}}class Gl{constructor(t){this.items=[],this.pool=[],this.particleOptions=t,this.particleFactory=new Ql}update(){const t=[],n=[];this.items.forEach(s=>{s.update(),s.pastBottom()?s.remove||(s.setup(this.particleOptions),t.push(s)):n.push(s)}),this.pool.push(...t),this.items=n}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 jn{constructor(t){const n="confetti-canvas";if(t&&!(t instanceof HTMLCanvasElement))throw new Error("Element is not a valid HTMLCanvasElement");this.isDefault=!t,this.canvas=t||document.getElementById(n)||jn.createDefaultCanvas(n),this.ctx=this.canvas.getContext("2d")}static createDefaultCanvas(t){const n=document.createElement("canvas");return n.style.display="block",n.style.position="fixed",n.style.pointerEvents="none",n.style.top=0,n.style.width="100vw",n.style.height="100vh",n.id=t,document.querySelector("body").appendChild(n),n}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 eo{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 n={canvas:this.canvas,W:this.W,H:this.H,wind:this.wind,windPosCoef:this.windPosCoef,windSpeedMax:this.windSpeedMax,count:0};return Object.assign(n,t),n}createParticles(t={}){const n=this.getParticleOptions(t);this.particleManager=new Gl(n)}getCanvasElementFromOptions(t){const{canvasId:n,canvasElement:s}=t;let i=s;if(s&&!(s instanceof HTMLCanvasElement))throw new Error("Invalid options: canvasElement is not a valid HTMLCanvasElement");if(n&&s)throw new Error("Invalid options: canvasId and canvasElement are mutually exclusive");if(n&&!i&&(i=document.getElementById(n)),n&&!(i instanceof HTMLCanvasElement))throw new Error(`Invalid options: element with id "${n}" is not a valid HTMLCanvasElement`);return i}start(t={}){this.remove();const n=this.getCanvasElementFromOptions(t);this.canvas=new jn(n),this.canvasEl=n,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 n=this.getCanvasElementFromOptions(t);if(this.canvas&&n!==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 n=this.framesSinceDrop*this.particlesPerFrame;for(;n>=1;)this.particleManager.add(),n-=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 to=Vs({__name:"ConfettiParty",setup(e){const t={defaultType:"rect",defaultSize:15,defaultColors:["DodgerBlue","OliveDrab","Gold","pink","SlateBlue","lightblue","Violet","PaleGreen","SteelBlue","SandyBrown","Chocolate","Crimson"]},n=new eo;return Hn(()=>{n.start(t),setTimeout(()=>{n.stop()},5e3)}),(s,i)=>(ri(),ll("div"))}}),no=Vs({__name:"App",setup(e){return(t,n)=>(ri(),ol(to))}}),so=async()=>ql(no).mount("#app-container");so().then(()=>{console.log()}); 2 | //# sourceMappingURL=welcome-71d12b53.js.map 3 | --------------------------------------------------------------------------------