From 565808013d1762da4beb504a7695a59cdf7b2285 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula <terabytesoftw@gmail.com>
Date: Sat, 25 Nov 2023 21:20:33 -0300
Subject: [PATCH 1/5] Add api for set cookie theme.

---
 composer.json                                 |   3 +-
 composer.lock                                 | 307 ++++++++++++++++--
 config/params-web.php                         |   4 +
 public/index.php                              |   2 +-
 src/Framework/resource/js/toggle-theme.js     | 192 ++++++-----
 .../layout/component/toggle_theme.php         |   9 +-
 src/UseCase/Api/ApiController.php             |  41 +++
 src/UseCase/Api/Theme/ThemeAction.php         |  41 +++
 src/UseCase/Contact/ContactController.php     |   5 +-
 tests/Functional.suite.yml                    |   6 +
 tests/Functional/ThemeActionCest.php          |  36 ++
 tests/Support/ApiTester.php                   |  29 ++
 tests/_bootstrap.php                          |   2 +-
 13 files changed, 557 insertions(+), 120 deletions(-)
 create mode 100644 src/UseCase/Api/ApiController.php
 create mode 100644 src/UseCase/Api/Theme/ThemeAction.php
 create mode 100644 tests/Functional/ThemeActionCest.php
 create mode 100644 tests/Support/ApiTester.php

diff --git a/composer.json b/composer.json
index 364cf92..6070168 100644
--- a/composer.json
+++ b/composer.json
@@ -39,7 +39,8 @@
         "symfony/process": "^6.3",
         "yii2-extensions/debug": "dev-improve-config",
         "yii2-extensions/gii": "dev-main",
-        "yii2-extensions/phpstan": "dev-main"
+        "yii2-extensions/phpstan": "dev-main",
+        "codeception/module-rest": "^3.3"
     },
     "autoload": {
         "psr-4": {
diff --git a/composer.lock b/composer.lock
index 503ead5..0f4eaca 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "508854391f777d23a09762490ada9e7a",
+    "content-hash": "fd8c01c2122acfc766ba5d07791f7943",
     "packages": [
         {
             "name": "bower-asset/inputmask",
@@ -2199,12 +2199,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/yii2-extensions/asset-bootstrap5.git",
-                "reference": "4fced22d51977b157dffd1a3c1868b2990b0d689"
+                "reference": "c5e43b3a3e1a1ee89e5479cb2b112b61d2aa45c6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/yii2-extensions/asset-bootstrap5/zipball/4fced22d51977b157dffd1a3c1868b2990b0d689",
-                "reference": "4fced22d51977b157dffd1a3c1868b2990b0d689",
+                "url": "https://api.github.com/repos/yii2-extensions/asset-bootstrap5/zipball/c5e43b3a3e1a1ee89e5479cb2b112b61d2aa45c6",
+                "reference": "c5e43b3a3e1a1ee89e5479cb2b112b61d2aa45c6",
                 "shasum": ""
             },
             "require": {
@@ -2212,12 +2212,13 @@
                 "npm-asset/popperjs--core": "^2.11",
                 "oomphinc/composer-installers-extender": "^2.0",
                 "php": ">=8.1",
-                "yiisoft/yii2": "^2.2"
+                "yiisoft/yii2": "*"
             },
             "require-dev": {
                 "maglnet/composer-require-checker": "^4.6",
                 "php-forge/support": "dev-main",
                 "phpunit/phpunit": "^10.2",
+                "roave/infection-static-analysis-plugin": "^1.32",
                 "yii2-extensions/phpstan": "dev-main"
             },
             "default-branch": true,
@@ -2256,7 +2257,7 @@
                 "issues": "https://github.com/yii2-extensions/asset-bootstrap5/issues",
                 "source": "https://github.com/yii2-extensions/asset-bootstrap5/tree/main"
             },
-            "time": "2023-11-17T21:00:37+00:00"
+            "time": "2023-11-19T14:17:44+00:00"
         },
         {
             "name": "yii2-extensions/bootstrap5",
@@ -2332,18 +2333,18 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/yii2-extensions/localeurls.git",
-                "reference": "dd92af04aabeb32a027c2bffdb0a6563ce74e37a"
+                "reference": "dd886bae0b41615f5917602225e2dbd766f8960f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/yii2-extensions/localeurls/zipball/dd92af04aabeb32a027c2bffdb0a6563ce74e37a",
-                "reference": "dd92af04aabeb32a027c2bffdb0a6563ce74e37a",
+                "url": "https://api.github.com/repos/yii2-extensions/localeurls/zipball/dd886bae0b41615f5917602225e2dbd766f8960f",
+                "reference": "dd886bae0b41615f5917602225e2dbd766f8960f",
                 "shasum": ""
             },
             "require": {
                 "ext-mbstring": "*",
                 "php": ">=8.1",
-                "yiisoft/yii2": "^2.2"
+                "yiisoft/yii2": "*"
             },
             "require-dev": {
                 "maglnet/composer-require-checker": "^4.6",
@@ -2351,7 +2352,7 @@
                 "yii2-extensions/phpstan": "dev-main"
             },
             "default-branch": true,
-            "type": "yii2-extension",
+            "type": "library",
             "extra": {
                 "branch-alias": {
                     "dev-main": "1.0.x-dev"
@@ -2366,7 +2367,7 @@
             },
             "autoload": {
                 "psr-4": {
-                    "yii\\localeurls\\": "src"
+                    "Yii2\\Extensions\\LocaleUrls\\": "src"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
@@ -2383,7 +2384,7 @@
                 "issues": "https://github.com/yii2-extensions/localeurls/issues",
                 "source": "https://github.com/yii2-extensions/localeurls/tree/main"
             },
-            "time": "2023-11-18T11:58:39+00:00"
+            "time": "2023-11-20T10:48:28+00:00"
         },
         {
             "name": "yiisoft/arrays",
@@ -3338,6 +3339,56 @@
             },
             "time": "2023-04-18T20:32:51+00:00"
         },
+        {
+            "name": "codeception/lib-xml",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Codeception/lib-xml.git",
+                "reference": "d6e4c094fb83958bcf254a20815cea5ac31e98d0"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Codeception/lib-xml/zipball/d6e4c094fb83958bcf254a20815cea5ac31e98d0",
+                "reference": "d6e4c094fb83958bcf254a20815cea5ac31e98d0",
+                "shasum": ""
+            },
+            "require": {
+                "codeception/lib-web": "^1.0",
+                "ext-dom": "*",
+                "php": "^8.0",
+                "phpunit/phpunit": "^9.5 | ^10.0",
+                "symfony/css-selector": ">=4.4.24 <7.0"
+            },
+            "conflict": {
+                "codeception/codeception": "<5.0.0-alpha3"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Gintautas Miselis"
+                }
+            ],
+            "description": "Files used by module-rest and module-soap",
+            "homepage": "https://codeception.com/",
+            "keywords": [
+                "codeception"
+            ],
+            "support": {
+                "issues": "https://github.com/Codeception/lib-xml/issues",
+                "source": "https://github.com/Codeception/lib-xml/tree/1.0.1"
+            },
+            "time": "2022-09-11T14:09:09+00:00"
+        },
         {
             "name": "codeception/module-asserts",
             "version": "3.0.0",
@@ -3447,6 +3498,66 @@
             },
             "time": "2022-03-14T18:48:55+00:00"
         },
+        {
+            "name": "codeception/module-rest",
+            "version": "3.3.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Codeception/module-rest.git",
+                "reference": "bb545d4f7c261472472da8730267d9df162199cb"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Codeception/module-rest/zipball/bb545d4f7c261472472da8730267d9df162199cb",
+                "reference": "bb545d4f7c261472472da8730267d9df162199cb",
+                "shasum": ""
+            },
+            "require": {
+                "codeception/codeception": "^5.0.8",
+                "codeception/lib-xml": "^1.0",
+                "ext-dom": "*",
+                "ext-json": "*",
+                "justinrainbow/json-schema": "~5.2.9",
+                "php": "^8.0",
+                "softcreatr/jsonpath": "^0.8"
+            },
+            "require-dev": {
+                "codeception/lib-innerbrowser": "^3.0 | ^4.0",
+                "codeception/stub": "^4.0",
+                "codeception/util-universalframework": "^1.0",
+                "ext-libxml": "*",
+                "ext-simplexml": "*"
+            },
+            "suggest": {
+                "aws/aws-sdk-php": "For using AWS Auth"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Gintautas Miselis"
+                }
+            ],
+            "description": "REST module for Codeception",
+            "homepage": "https://codeception.com/",
+            "keywords": [
+                "codeception",
+                "rest"
+            ],
+            "support": {
+                "issues": "https://github.com/Codeception/module-rest/issues",
+                "source": "https://github.com/Codeception/module-rest/tree/3.3.2"
+            },
+            "time": "2023-02-09T18:11:19+00:00"
+        },
         {
             "name": "codeception/module-yii2",
             "version": "1.1.9",
@@ -3713,6 +3824,76 @@
             ],
             "time": "2023-08-27T10:13:57+00:00"
         },
+        {
+            "name": "justinrainbow/json-schema",
+            "version": "v5.2.13",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/justinrainbow/json-schema.git",
+                "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/fbbe7e5d79f618997bc3332a6f49246036c45793",
+                "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1",
+                "json-schema/json-schema-test-suite": "1.2.0",
+                "phpunit/phpunit": "^4.8.35"
+            },
+            "bin": [
+                "bin/validate-json"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "5.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "JsonSchema\\": "src/JsonSchema/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Bruno Prieto Reis",
+                    "email": "bruno.p.reis@gmail.com"
+                },
+                {
+                    "name": "Justin Rainbow",
+                    "email": "justin.rainbow@gmail.com"
+                },
+                {
+                    "name": "Igor Wiedler",
+                    "email": "igor@wiedler.ch"
+                },
+                {
+                    "name": "Robert Schönthal",
+                    "email": "seroscho@googlemail.com"
+                }
+            ],
+            "description": "A library to validate a json schema.",
+            "homepage": "https://github.com/justinrainbow/json-schema",
+            "keywords": [
+                "json",
+                "schema"
+            ],
+            "support": {
+                "issues": "https://github.com/justinrainbow/json-schema/issues",
+                "source": "https://github.com/justinrainbow/json-schema/tree/v5.2.13"
+            },
+            "time": "2023-09-26T02:20:38+00:00"
+        },
         {
             "name": "maglnet/composer-require-checker",
             "version": "4.7.1",
@@ -4129,16 +4310,16 @@
         },
         {
             "name": "phpstan/phpstan",
-            "version": "1.10.42",
+            "version": "1.10.43",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan.git",
-                "reference": "fc2316508de5453140b5cb3d3f8683a33e92f26a"
+                "reference": "2c4129f6ca8c7cfa870098884b8869b410a5a361"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fc2316508de5453140b5cb3d3f8683a33e92f26a",
-                "reference": "fc2316508de5453140b5cb3d3f8683a33e92f26a",
+                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2c4129f6ca8c7cfa870098884b8869b410a5a361",
+                "reference": "2c4129f6ca8c7cfa870098884b8869b410a5a361",
                 "shasum": ""
             },
             "require": {
@@ -4187,7 +4368,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-11-17T15:26:57+00:00"
+            "time": "2023-11-19T19:55:25+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
@@ -5758,6 +5939,74 @@
             ],
             "time": "2023-02-07T11:34:05+00:00"
         },
+        {
+            "name": "softcreatr/jsonpath",
+            "version": "0.8.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/SoftCreatR/JSONPath.git",
+                "reference": "fc12dee0b46f3fa3a175c4051dbab60984acef4b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/SoftCreatR/JSONPath/zipball/fc12dee0b46f3fa3a175c4051dbab60984acef4b",
+                "reference": "fc12dee0b46f3fa3a175c4051dbab60984acef4b",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "php": ">=8.0"
+            },
+            "replace": {
+                "flow/jsonpath": "*"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.6",
+                "roave/security-advisories": "dev-latest"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Flow\\JSONPath\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Stephen Frank",
+                    "email": "stephen@flowsa.com",
+                    "homepage": "https://prismaticbytes.com",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sascha Greuel",
+                    "email": "hello@1-2.dev",
+                    "homepage": "https://1-2.dev",
+                    "role": "Developer"
+                }
+            ],
+            "description": "JSONPath implementation for parsing, searching and flattening arrays",
+            "support": {
+                "email": "hello@1-2.dev",
+                "forum": "https://github.com/SoftCreatR/JSONPath/discussions",
+                "issues": "https://github.com/SoftCreatR/JSONPath/issues",
+                "source": "https://github.com/SoftCreatR/JSONPath"
+            },
+            "funding": [
+                {
+                    "url": "https://ecologi.com/softcreatr?r=61212ab3fc69b8eb8a2014f4",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/softcreatr",
+                    "type": "github"
+                }
+            ],
+            "time": "2023-08-17T20:14:00+00:00"
+        },
         {
             "name": "symfony/browser-kit",
             "version": "v6.3.8",
@@ -6499,16 +6748,16 @@
         },
         {
             "name": "theseer/tokenizer",
-            "version": "1.2.1",
+            "version": "1.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/theseer/tokenizer.git",
-                "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e"
+                "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e",
-                "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e",
+                "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96",
+                "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96",
                 "shasum": ""
             },
             "require": {
@@ -6537,7 +6786,7 @@
             "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
             "support": {
                 "issues": "https://github.com/theseer/tokenizer/issues",
-                "source": "https://github.com/theseer/tokenizer/tree/1.2.1"
+                "source": "https://github.com/theseer/tokenizer/tree/1.2.2"
             },
             "funding": [
                 {
@@ -6545,7 +6794,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2021-07-28T10:34:58+00:00"
+            "time": "2023-11-20T00:12:19+00:00"
         },
         {
             "name": "webmozart/assert",
@@ -6781,19 +7030,19 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/yii2-extensions/phpstan.git",
-                "reference": "a2cf494866c35b3586ee8bc4d6824d91abb12141"
+                "reference": "88b84a5851cb12361c93b1cae6dcb9b95a2dedd3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/yii2-extensions/phpstan/zipball/a2cf494866c35b3586ee8bc4d6824d91abb12141",
-                "reference": "a2cf494866c35b3586ee8bc4d6824d91abb12141",
+                "url": "https://api.github.com/repos/yii2-extensions/phpstan/zipball/88b84a5851cb12361c93b1cae6dcb9b95a2dedd3",
+                "reference": "88b84a5851cb12361c93b1cae6dcb9b95a2dedd3",
                 "shasum": ""
             },
             "require": {
                 "nikic/php-parser": "^4.1.0",
                 "php": ">=8.1",
                 "phpstan/phpstan": "^1.0",
-                "yiisoft/yii2": "^2.2"
+                "yiisoft/yii2": "*"
             },
             "require-dev": {
                 "maglnet/composer-require-checker": "^4.6",
@@ -6825,7 +7074,7 @@
                 "issues": "https://github.com/yii2-extensions/phpstan/issues",
                 "source": "https://github.com/yii2-extensions/phpstan/tree/main"
             },
-            "time": "2023-10-11T09:59:22+00:00"
+            "time": "2023-11-19T14:24:04+00:00"
         }
     ],
     "aliases": [],
diff --git a/config/params-web.php b/config/params-web.php
index 9d6ac36..eda77cd 100644
--- a/config/params-web.php
+++ b/config/params-web.php
@@ -2,6 +2,7 @@
 
 declare(strict_types=1);
 
+use App\UseCase\Api\ApiController;
 use App\UseCase\Contact\ContactController;
 use App\UseCase\Site\SiteController;
 
@@ -16,6 +17,9 @@
     ],
     'app.assetManager.basePath' => '@public/assets',
     'app.controllerMap' => [
+        'api' => [
+            'class' => ApiController::class,
+        ],
         'contact' => [
             'class' => ContactController::class,
         ],
diff --git a/public/index.php b/public/index.php
index f567043..7a65744 100644
--- a/public/index.php
+++ b/public/index.php
@@ -8,7 +8,7 @@
 use Yiisoft\Config\Modifier\RecursiveMerge;
 
 // comment out the following two lines when deployed to production
-defined('YII_DEBUG') or define('YII_DEBUG', false);
+defined('YII_DEBUG') or define('YII_DEBUG', true);
 
 if (getenv('YII_ENV')) {
     defined('YII_ENV') or define('YII_ENV', getenv('YII_ENV'));
diff --git a/src/Framework/resource/js/toggle-theme.js b/src/Framework/resource/js/toggle-theme.js
index a48b6be..1ca5e91 100644
--- a/src/Framework/resource/js/toggle-theme.js
+++ b/src/Framework/resource/js/toggle-theme.js
@@ -4,86 +4,114 @@
  * Licensed under the Creative Commons Attribution 3.0 Unported License.
  */
 (() => {
-  'use strict'
-
-  const getStoredTheme = () => localStorage.getItem('theme')
-  const setStoredTheme = theme => localStorage.setItem('theme', theme)
-
-  const getPreferredTheme = () => {
-    const storedTheme = getStoredTheme()
-    if (storedTheme) {
-      return storedTheme
-    }
-
-    return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
-  }
-
-  const setTheme = theme => {
-    if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
-      document.documentElement.setAttribute('data-bs-theme', 'dark')
-    } else {
-      document.documentElement.setAttribute('data-bs-theme', theme)
-    }
-  }
-
-  setTheme(getPreferredTheme())
-
-  const showActiveTheme = (theme, focus = false) => {
-    const themeSwitcher = document.querySelector('#toggle-theme')
-
-    if (!themeSwitcher) {
-      return
-    }
-
-    const themeSwitcherText = document.querySelector('#toggle-theme-text')
-    const activeThemeIcon = document.querySelector('.theme-icon-active use')
-    const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
-    const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href')
-
-    // Añade las siguientes líneas para mostrar el icono de verificación en el botón activo.
-    const activeCheckIcon = btnToActive.querySelector('.check');
-    activeCheckIcon.classList.remove('d-none');
-
-    document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
-      const checkIcon = element.querySelector('.check');
-      if (element === btnToActive) {
-        checkIcon.classList.remove('d-none');
-      } else {
-        checkIcon.classList.add('d-none');
-      }
-      element.classList.remove('active')
-      element.setAttribute('aria-pressed', 'false')
-    })
-
-    btnToActive.classList.add('active')
-    btnToActive.setAttribute('aria-pressed', 'true')
-    activeThemeIcon.setAttribute('href', svgOfActiveBtn)
-    const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`
-    themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)
-
-    if (focus) {
-      themeSwitcher.focus()
-    }
-  }
-
-  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
-    const storedTheme = getStoredTheme()
-    if (storedTheme !== 'light' && storedTheme !== 'dark') {
-      setTheme(getPreferredTheme())
-    }
-  })
-
-  window.addEventListener('DOMContentLoaded', () => {
-    showActiveTheme(getPreferredTheme())
-
-    document.querySelectorAll('[data-bs-theme-value]')
-      .forEach(toggle => {
-        toggle.addEventListener('click', () => {
-          const theme = toggle.getAttribute('data-bs-theme-value')
-          setStoredTheme(theme)
-          setTheme(theme)
-          showActiveTheme(theme, true)
+    'use strict';
+
+    const getStoredTheme = () => localStorage.getItem('theme');
+    const setStoredTheme = (theme) => localStorage.setItem('theme', theme);
+
+    const getPreferredTheme = () => {
+        const storedTheme = getStoredTheme();
+
+        if (storedTheme) {
+            return storedTheme;
+        }
+
+        return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+    };
+
+    const setTheme = (theme) => {
+        if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
+            document.documentElement.setAttribute('data-bs-theme', 'dark');
+        } else {
+            document.documentElement.setAttribute('data-bs-theme', theme);
+        }
+    };
+
+    setTheme(getPreferredTheme());
+
+    const showActiveTheme = (theme, focus = false) => {
+        const themeSwitcher = document.querySelector('#toggle-theme');
+
+        if (!themeSwitcher) {
+            return;
+        }
+
+        const themeSwitcherText = document.querySelector('#toggle-theme-text');
+        const activeThemeIcon = document.querySelector('.theme-icon-active use');
+        const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`);
+        const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href');
+
+        // Añade las siguientes líneas para mostrar el icono de verificación en el botón activo.
+        const activeCheckIcon = btnToActive.querySelector('.check');
+        activeCheckIcon.classList.remove('d-none');
+
+        document.querySelectorAll('[data-bs-theme-value]').forEach((element) => {
+            const checkIcon = element.querySelector('.check');
+
+            if (element === btnToActive) {
+                checkIcon.classList.remove('d-none');
+            } else {
+                checkIcon.classList.add('d-none');
+            }
+
+            element.classList.remove('active');
+            element.setAttribute('aria-pressed', 'false');
+        });
+
+        btnToActive.classList.add('active');
+        btnToActive.setAttribute('aria-pressed', 'true');
+        activeThemeIcon.setAttribute('href', svgOfActiveBtn);
+        const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`;
+        themeSwitcher.setAttribute('aria-label', themeSwitcherLabel);
+
+        if (focus) {
+            themeSwitcher.focus();
+        }
+    };
+
+    const sendTheme = (theme) => {
+        const languageCode = document.documentElement.getAttribute('lang').substring(0, 2);
+        const baseUrl = '/api/theme';
+        const url = `/${languageCode}${baseUrl}`;
+
+        fetch(url, {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json',
+                'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
+            },
+            body: JSON.stringify({ theme }),
+        })
+        .then((response) => {
+            if (!response.ok) {
+                console.error('Failed to update theme');
+            }
         })
-      })
-  })
-})()
+        .catch((error) => {
+            console.error('Error updating theme:', error);
+        });
+    };
+
+    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
+        const storedTheme = getStoredTheme();
+
+        if (storedTheme !== 'light' && storedTheme !== 'dark') {
+            setTheme(getPreferredTheme());
+        }
+    });
+
+    window.addEventListener('DOMContentLoaded', () => {
+        showActiveTheme(getPreferredTheme());
+
+        document.querySelectorAll('[data-bs-theme-value]').forEach((toggle) => {
+            toggle.addEventListener('click', () => {
+                const theme = toggle.getAttribute('data-bs-theme-value');
+
+                setStoredTheme(theme);
+                setTheme(theme);
+                showActiveTheme(theme, true);
+                sendTheme(theme);
+            });
+        });
+    });
+  })();
diff --git a/src/Framework/resource/layout/component/toggle_theme.php b/src/Framework/resource/layout/component/toggle_theme.php
index 5d5e721..6e59155 100644
--- a/src/Framework/resource/layout/component/toggle_theme.php
+++ b/src/Framework/resource/layout/component/toggle_theme.php
@@ -7,6 +7,7 @@
 use PHPForge\Component\Item;
 use PHPForge\Html\Span;
 use sjaakp\icon\Icon;
+use yii\helpers\Url;
 use yii\web\View;
 
 /**
@@ -14,6 +15,8 @@
  */
 ToggleThemeAsset::register($this);
 
+$link = $this->context->action->uniqueId === 'site/index' ? Url::home() : Url::current([]);
+
 echo Dropdown::widget()
     ->container(true)
     ->containerClass('btn-group dropup bd-mode-toggle ms-3')
@@ -28,7 +31,7 @@
                     ['class' => 'check ms-auto d-none', 'width' => '1em', 'height' => '1em'],
                 ),
             )
-            ->link('#')
+            ->link($link)
             ->linkAttributes(['aria-pressed' => 'false', 'data-bs-theme-value' => 'light'])
             ->linkClass('dropdown-item d-flex align-items-center'),
         Item::create()
@@ -41,7 +44,7 @@
                     ['class' => 'check ms-auto d-none', 'width' => '1em', 'height' => '1em'],
                 ),
             )
-            ->link('#')
+            ->link($link)
             ->linkAttributes(['aria-pressed' => 'false', 'data-bs-theme-value' => 'dark'])
             ->linkClass('dropdown-item d-flex align-items-center'),
         Item::create()
@@ -54,7 +57,7 @@
                     ['class' => 'check ms-auto d-none', 'width' => '1em', 'height' => '1em'],
                 ),
             )
-            ->link('#')
+            ->link($link)
             ->linkAttributes(['aria-pressed' => 'false', 'data-bs-theme-value' => 'auto'])
             ->linkClass('dropdown-item d-flex align-items-center'),
     )
diff --git a/src/UseCase/Api/ApiController.php b/src/UseCase/Api/ApiController.php
new file mode 100644
index 0000000..1f393f0
--- /dev/null
+++ b/src/UseCase/Api/ApiController.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\UseCase\Api;
+
+use App\UseCase\Api\Theme\ThemeAction;
+use yii\filters\VerbFilter;
+use yii\web\Controller;
+
+final class ApiController extends Controller
+{
+    public function actions(): array
+    {
+        return [
+            'theme' => [
+                'class' => ThemeAction::class,
+            ],
+        ];
+    }
+
+    public function beforeAction($action) {
+        if(YII_ENV == 'test' && $action->id == 'theme') {
+            $this->enableCsrfValidation = false;
+        }
+
+        return parent::beforeAction($action);
+    }
+
+    public function behaviors(): array
+    {
+        return [
+            'verbs' => [
+                'class' => VerbFilter::class,
+                'actions' => [
+                    'theme' => ['POST'],
+                ],
+            ],
+        ];
+    }
+}
diff --git a/src/UseCase/Api/Theme/ThemeAction.php b/src/UseCase/Api/Theme/ThemeAction.php
new file mode 100644
index 0000000..4dfc537
--- /dev/null
+++ b/src/UseCase/Api/Theme/ThemeAction.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\UseCase\Api\Theme;
+
+use yii\base\Action;
+use yii\helpers\Json;
+use yii\web\Cookie;
+use yii\web\Response;
+
+final class ThemeAction extends Action
+{
+    public function run(): array
+    {
+        $this->controller->response->format = Response::FORMAT_JSON;
+
+        $theme = Json::decode($this->controller->request->getRawBody());
+
+        if (isset($theme['theme'])) {
+            $cookie = $this->controller->request->cookies->get('theme');
+
+            if ($cookie !== null) {
+                $cookie->value = $theme['theme'];
+                $this->controller->response->cookies->add($cookie);
+            } else {
+                $cookie = new Cookie(
+                    [
+                        'name' => 'theme',
+                        'value' => $theme['theme'],
+                        'expire' => time() + 86400, // 1 day
+                    ],
+                );
+
+                $this->controller->response->cookies->add($cookie);
+            }
+        }
+
+        return $theme;
+    }
+}
diff --git a/src/UseCase/Contact/ContactController.php b/src/UseCase/Contact/ContactController.php
index f8dcd20..8d54377 100644
--- a/src/UseCase/Contact/ContactController.php
+++ b/src/UseCase/Contact/ContactController.php
@@ -20,7 +20,7 @@ public function actions(): array
             [
                 'captcha' => [
                     'class' => CaptchaAction::class,
-                    'fixedVerifyCode' => (YII_ENV === 'tests') ? 'testme' : null,
+                    'fixedVerifyCode' => (YII_ENV === 'test') ? 'testme' : null,
                 ],
             ],
             parent::actions(),
@@ -43,8 +43,7 @@ public function actionIndex(): Response|string
     {
         if (
             $this->request instanceof Request &&
-            $this->contactForm->load($this->request->post()) &&
-            $this->contactForm->validate()
+            $this->contactForm->load($this->request->post()) && $this->contactForm->validate()
         ) {
             if ($this->contactForm->sendContact($this->mailer, $this->module->params)) {
                 $this->session->setFlash('contactFormSubmitted');
diff --git a/tests/Functional.suite.yml b/tests/Functional.suite.yml
index 3e33c70..36becf6 100644
--- a/tests/Functional.suite.yml
+++ b/tests/Functional.suite.yml
@@ -10,4 +10,10 @@ modules:
       - Filesystem
       - Yii2
       - Asserts
+      - REST:
+          url: http://localhost:8080
+          depends: Yii2
+          part: Json
+          headers:
+              Content-Type: application/json
 step_decorators: ~
diff --git a/tests/Functional/ThemeActionCest.php b/tests/Functional/ThemeActionCest.php
new file mode 100644
index 0000000..434eb98
--- /dev/null
+++ b/tests/Functional/ThemeActionCest.php
@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Tests\Functional;
+
+use App\Tests\Support\FunctionalTester;
+use Yii;
+
+final class ThemeActionCest
+{
+    public function testTheme(FunctionalTester $I): void
+    {
+        $theme = ['theme' => 'dark'];
+
+        $I->sendPost('/api/theme', json_encode($theme));
+        $I->seeResponseCodeIsSuccessful();
+        $I->seeResponseContainsJson($theme);
+        $I->seeCookie('theme');
+    }
+
+    public function testThemeWithCookie(FunctionalTester $I): void
+    {
+        $theme = ['theme' => 'dark'];
+
+        $I->sendPost('/api/theme', json_encode($theme));
+        $I->seeResponseCodeIsSuccessful();
+        $I->seeResponseContainsJson($theme);
+        $I->seeCookie('theme');
+
+        $I->sendPost('/api/theme', json_encode($theme));
+        $I->seeResponseCodeIsSuccessful();
+        $I->seeResponseContainsJson($theme);
+        $I->seeCookie('theme');
+    }
+}
diff --git a/tests/Support/ApiTester.php b/tests/Support/ApiTester.php
new file mode 100644
index 0000000..bdf017b
--- /dev/null
+++ b/tests/Support/ApiTester.php
@@ -0,0 +1,29 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Tests\Support;
+
+/**
+ * Inherited Methods
+ * @method void wantTo($text)
+ * @method void wantToTest($text)
+ * @method void execute($callable)
+ * @method void expectTo($prediction)
+ * @method void expect($prediction)
+ * @method void amGoingTo($argumentation)
+ * @method void am($role)
+ * @method void lookForwardTo($achieveValue)
+ * @method void comment($description)
+ * @method void pause($vars = [])
+ *
+ * @SuppressWarnings(PHPMD)
+*/
+class ApiTester extends \Codeception\Actor
+{
+    use _generated\ApiTesterActions;
+
+    /**
+     * Define custom actions here
+     */
+}
diff --git a/tests/_bootstrap.php b/tests/_bootstrap.php
index 0f260c2..72b0fed 100644
--- a/tests/_bootstrap.php
+++ b/tests/_bootstrap.php
@@ -1,6 +1,6 @@
 <?php
 
-define('YII_ENV', 'tests');
+define('YII_ENV', 'test');
 defined('YII_DEBUG') or define('YII_DEBUG', true);
 
 require_once __DIR__ . '/../vendor/yiisoft/yii2/Yii.php';

From 32882b445ddfcb3851e13c16fbf80fdf3922d190 Mon Sep 17 00:00:00 2001
From: StyleCI Bot <bot@styleci.io>
Date: Sun, 26 Nov 2023 00:20:56 +0000
Subject: [PATCH 2/5] Apply fixes from StyleCI

---
 src/UseCase/Api/ApiController.php | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/UseCase/Api/ApiController.php b/src/UseCase/Api/ApiController.php
index 1f393f0..26660b2 100644
--- a/src/UseCase/Api/ApiController.php
+++ b/src/UseCase/Api/ApiController.php
@@ -19,8 +19,9 @@ public function actions(): array
         ];
     }
 
-    public function beforeAction($action) {
-        if(YII_ENV == 'test' && $action->id == 'theme') {
+    public function beforeAction($action)
+    {
+        if (YII_ENV == 'test' && $action->id == 'theme') {
             $this->enableCsrfValidation = false;
         }
 

From 205820e0b379a6e0be91eb33c61e0d1d20a293ca Mon Sep 17 00:00:00 2001
From: Wilmer Arambula <terabytesoftw@gmail.com>
Date: Sat, 25 Nov 2023 21:22:23 -0300
Subject: [PATCH 3/5] Add type hint return.

---
 src/UseCase/Api/ApiController.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/UseCase/Api/ApiController.php b/src/UseCase/Api/ApiController.php
index 26660b2..b4cd329 100644
--- a/src/UseCase/Api/ApiController.php
+++ b/src/UseCase/Api/ApiController.php
@@ -19,7 +19,7 @@ public function actions(): array
         ];
     }
 
-    public function beforeAction($action)
+    public function beforeAction($action): bool
     {
         if (YII_ENV == 'test' && $action->id == 'theme') {
             $this->enableCsrfValidation = false;

From d30981677584c84f94535087a90a18e753a04663 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula <terabytesoftw@gmail.com>
Date: Sat, 25 Nov 2023 21:37:17 -0300
Subject: [PATCH 4/5] Fix phpstan issues.

---
 .../layout/component/toggle_theme.php         |  4 +-
 src/UseCase/Api/Theme/ThemeAction.php         | 47 ++++++++++---------
 2 files changed, 29 insertions(+), 22 deletions(-)

diff --git a/src/Framework/resource/layout/component/toggle_theme.php b/src/Framework/resource/layout/component/toggle_theme.php
index 6e59155..e58eac3 100644
--- a/src/Framework/resource/layout/component/toggle_theme.php
+++ b/src/Framework/resource/layout/component/toggle_theme.php
@@ -9,13 +9,15 @@
 use sjaakp\icon\Icon;
 use yii\helpers\Url;
 use yii\web\View;
+use yii\base\Action;
 
 /**
  * @var View $this
  */
 ToggleThemeAsset::register($this);
 
-$link = $this->context->action->uniqueId === 'site/index' ? Url::home() : Url::current([]);
+/** @phpstan-ignore-next-line */
+$link = $this->context->action->getUniqueId() === 'site/index' ? Url::home() : Url::current([]);
 
 echo Dropdown::widget()
     ->container(true)
diff --git a/src/UseCase/Api/Theme/ThemeAction.php b/src/UseCase/Api/Theme/ThemeAction.php
index 4dfc537..684bed8 100644
--- a/src/UseCase/Api/Theme/ThemeAction.php
+++ b/src/UseCase/Api/Theme/ThemeAction.php
@@ -7,35 +7,40 @@
 use yii\base\Action;
 use yii\helpers\Json;
 use yii\web\Cookie;
+use yii\web\Request;
 use yii\web\Response;
 
 final class ThemeAction extends Action
 {
     public function run(): array
     {
-        $this->controller->response->format = Response::FORMAT_JSON;
-
-        $theme = Json::decode($this->controller->request->getRawBody());
-
-        if (isset($theme['theme'])) {
-            $cookie = $this->controller->request->cookies->get('theme');
-
-            if ($cookie !== null) {
-                $cookie->value = $theme['theme'];
-                $this->controller->response->cookies->add($cookie);
-            } else {
-                $cookie = new Cookie(
-                    [
-                        'name' => 'theme',
-                        'value' => $theme['theme'],
-                        'expire' => time() + 86400, // 1 day
-                    ],
-                );
-
-                $this->controller->response->cookies->add($cookie);
+        if ($this->controller->response instanceof Response && $this->controller->request instanceof Request) {
+            $this->controller->response->format = Response::FORMAT_JSON;
+
+            $this->controller->response->format = Response::FORMAT_JSON;
+
+            $theme = Json::decode($this->controller->request->getRawBody());
+
+            if (isset($theme['theme'])) {
+                $cookie = $this->controller->request->cookies->get('theme');
+
+                if ($cookie !== null) {
+                    $cookie->value = $theme['theme'];
+                    $this->controller->response->cookies->add($cookie);
+                } else {
+                    $cookie = new Cookie(
+                        [
+                            'name' => 'theme',
+                            'value' => $theme['theme'],
+                            'expire' => time() + 86400, // 1 day
+                        ],
+                    );
+
+                    $this->controller->response->cookies->add($cookie);
+                }
             }
         }
 
-        return $theme;
+        return $theme ?? [];
     }
 }

From fee0e1f19e594ee7ad3442a49c248f63f7d3c8e5 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula <terabytesoftw@gmail.com>
Date: Sat, 25 Nov 2023 21:38:07 -0300
Subject: [PATCH 5/5] Fix style.ci.

---
 src/Framework/resource/layout/component/toggle_theme.php | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/Framework/resource/layout/component/toggle_theme.php b/src/Framework/resource/layout/component/toggle_theme.php
index e58eac3..92c3008 100644
--- a/src/Framework/resource/layout/component/toggle_theme.php
+++ b/src/Framework/resource/layout/component/toggle_theme.php
@@ -9,7 +9,6 @@
 use sjaakp\icon\Icon;
 use yii\helpers\Url;
 use yii\web\View;
-use yii\base\Action;
 
 /**
  * @var View $this