├── 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 | [](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 | 
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 | Get Started
47 |
48 |
49 |
50 |
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 | [
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 | 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
333 | */
334 | protected function customFrontendRoutes(): array
335 | {
336 | return [
337 | 'instantanalytics/pageViewTrack/?' =>
338 | 'instant-analytics/track/track-page-view-url',
339 | 'instantanalytics/eventTrack/?' =>
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
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 | 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 | 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!!n[i.toLowerCase()]:i=>!!n[i]}function _n(e){if(A(e)){const t={};for(let n=0;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{},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{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{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{const{deps:t}=e;if(t.length){let n=0;for(let s=0;s{(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{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)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>>1;pt(Q[s])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(;tpt(n)-pt(s)),ze=0;zee.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;veX(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;we.__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{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;r1)for(let r=0;r!!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;le?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;n0)&&!(l&16)){if(l&8){const g=e.vnode.dynamicProps;for(let w=0;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-1,O[1]=M<0||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{for(let m=x;m{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{I&&xe(I,u,f,o),E&&Ue(f,o,u,"updated")},p)},z=(o,f,u,p,d,b,y)=>{for(let _=0;_{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;TE?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=IT)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){_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{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_.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;yo.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>1,e[n[c]]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;nV||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?`${e} `: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;ien||(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 |
--------------------------------------------------------------------------------