diff --git a/Dockerfile b/Dockerfile index 9f926be9b446d0bcb7f6f1a1f6c0cc0823d6966a..a2494d5aeefb9d90acfdde22854324deb7301cd6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,18 @@ -FROM python:3.10 +FROM python:3.11 RUN mkdir /app WORKDIR /app -RUN curl -fsSL https://deb.nodesource.com/setup_19.x | bash - -RUN apt-get install nodejs && rm -rf /var/lib/apt/lists/* +# Install NodeJS +ENV NODE_MAJOR=20 +RUN apt-get update +RUN apt-get install -y ca-certificates curl gnupg +RUN mkdir -p /etc/apt/keyrings +RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg +RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list +RUN apt-get update +RUN apt-get install -y nodejs +RUN rm -rf /var/lib/apt/lists/* COPY . . diff --git a/Makefile b/Makefile index 0aa344656622e2bcda54d6f306af6721624dc697..13b1223cfb36e3becb48de43196b7f990bc1b3ec 100644 --- a/Makefile +++ b/Makefile @@ -29,11 +29,11 @@ venv: .venv/bin/python install: venv ${VENV}/bin/pip install -r requirements/base.txt -r requirements/production.txt - ${VENV}/bin/npm install + npm install build: venv - ${VENV}/bin/npm run build + npm run build ${VENV}/bin/python manage.py collectstatic --noinput --settings=${SETTINGS} install-hooks: diff --git a/instagram_token/apps.py b/instagram_token/apps.py index 135a246c66478b2cdb299307c8b08b1c71354cc1..0b34638f148e695ad5b9c1c82c65d66922acb453 100644 --- a/instagram_token/apps.py +++ b/instagram_token/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class InstagramTokenConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'instagram_token' + default_auto_field = "django.db.models.BigAutoField" + name = "instagram_token" diff --git a/instagram_token/views.py b/instagram_token/views.py index 9f31e77dd1e0d1d82e3dc15538956ae68eaefb1e..0adee66debfe8429549d9c373f51284b517fb833 100644 --- a/instagram_token/views.py +++ b/instagram_token/views.py @@ -1,5 +1,4 @@ import requests - from django.conf import settings from django.shortcuts import render from django.urls import reverse @@ -20,11 +19,7 @@ def index(request): ) return render( - request, - "instagram_token/index.html", - { - "authorization_url": authorization_url - } + request, "instagram_token/index.html", {"authorization_url": authorization_url} ) @@ -41,8 +36,10 @@ def exchange(request): "client_secret": settings.INSTAGRAM_CLIENT_SECRET, "code": code, "grant_type": "authorization_code", - "redirect_uri": request.build_absolute_uri(reverse("instagram_token:exchange")), - } + "redirect_uri": request.build_absolute_uri( + reverse("instagram_token:exchange") + ), + }, ) if not exchange_request.ok: @@ -56,5 +53,5 @@ def exchange(request): { "access_token": exchange_request["access_token"], "user_id": exchange_request["user_id"], - } + }, ) diff --git a/package-lock.json b/package-lock.json index 1ffe84608e5db3d89a6ec42a46c5e2634989b1b0..4dfc02878e57699d1c2b527aff331c23221a0cb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,105 +9,29 @@ "version": "0.0.1", "license": "AGPL-3.0-or-later", "dependencies": { - "@tailwindcss/typography": "^0.4.1", + "@tailwindcss/typography": "^0.5.10", "alertifyjs": "^1.13.1", "css-loader": "^6.7.3", + "easytimer.js": "^4.5.4", "jquery": "^3.6.4", "js-cookie": "^3.0.1", "select2": "^4.1.0-rc.0", "style-loader": "^3.3.2", - "tailwindcss": "^2.2.2", + "tailwindcss": "^3.3.3", "webpack": "^5.77.0", "webpack-bundle-tracker": "^1.8.1", "webpack-cli": "^5.0.1" } }, - "node_modules/@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", - "dependencies": { - "@babel/highlight": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" + "node": ">=10" }, - "engines": { - "node": ">=4" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@discoveryjs/json-ext": { @@ -118,17 +42,6 @@ "node": ">=10.0.0" } }, - "node_modules/@fullhuman/postcss-purgecss": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-4.1.3.tgz", - "integrity": "sha512-jqcsyfvq09VOsMXxJMPLRF6Fhg/NNltzWKnC9qtzva+QKTxerCO4esG6je7hbnmkpZtaDyPTwMBj9bzfWorsrw==", - "dependencies": { - "purgecss": "^4.1.3" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", @@ -214,17 +127,29 @@ } }, "node_modules/@tailwindcss/typography": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.4.1.tgz", - "integrity": "sha512-ovPPLUhs7zAIJfr0y1dbGlyCuPhpuv/jpBoFgqAc658DWGGrOBWBMpAWLw2KlzbNeVk4YBJMzue1ekvIbdw6XA==", + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.10.tgz", + "integrity": "sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==", "dependencies": { "lodash.castarray": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.merge": "^4.6.2", - "lodash.uniq": "^4.5.0" + "postcss-selector-parser": "6.0.10" }, "peerDependencies": { - "tailwindcss": ">=2.0.0" + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" } }, "node_modules/@types/eslint": { @@ -260,11 +185,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" }, - "node_modules/@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" - }, "node_modules/@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -447,35 +367,6 @@ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, - "node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "dependencies": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - } - }, - "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -512,19 +403,10 @@ "node": ">=8" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" }, "node_modules/anymatch": { "version": "3.1.3", @@ -543,39 +425,6 @@ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" }, - "node_modules/autoprefixer": { - "version": "10.4.14", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", - "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - } - ], - "peer": true, - "dependencies": { - "browserslist": "^4.21.5", - "caniuse-lite": "^1.0.30001464", - "fraction.js": "^4.2.0", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -641,22 +490,6 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "engines": { - "node": ">=6" - } - }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -666,9 +499,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001481", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz", - "integrity": "sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ==", + "version": "1.0.30001547", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001547.tgz", + "integrity": "sha512-W7CrtIModMAxobGhz8iXmDfuJiiKg1WADMO/9x7/CLNin5cpSbuBjooyoIUVB5eyCc36QuTVlkVa1iB2S5+/eA==", "funding": [ { "type": "opencollective", @@ -684,32 +517,6 @@ } ] }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -768,53 +575,6 @@ "node": ">=6" } }, - "node_modules/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", - "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/color/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, "node_modules/colorette": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", @@ -830,21 +590,6 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -883,11 +628,6 @@ "webpack": "^5.0.0" } }, - "node_modules/css-unit-converter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", - "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==" - }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -899,30 +639,6 @@ "node": ">=4" } }, - "node_modules/defined": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", - "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/detective": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", - "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", - "dependencies": { - "acorn-node": "^1.8.2", - "defined": "^1.0.0", - "minimist": "^1.2.6" - }, - "bin": { - "detective": "bin/detective.js" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -933,6 +649,11 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, + "node_modules/easytimer.js": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/easytimer.js/-/easytimer.js-4.5.4.tgz", + "integrity": "sha512-65STy2sW2z6e9XwfJqSa18JVNWuOu2cb/FXaZ/BbiDiPnTvC53njMS3oY1BsAIm/Dzt9c8YUvcgc8FoPttm1Gw==" + }, "node_modules/electron-to-chromium": { "version": "1.4.284", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", @@ -961,14 +682,6 @@ "node": ">=4" } }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/es-module-lexer": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", @@ -982,14 +695,6 @@ "node": ">=6" } }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -1043,9 +748,9 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -1112,41 +817,15 @@ "node": ">=8" } }, - "node_modules/fraction.js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", - "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", - "peer": true, - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://www.patreon.com/infusion" - } - }, - "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, "optional": true, "os": [ @@ -1157,19 +836,22 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.1.1", + "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, @@ -1201,17 +883,6 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1220,15 +891,15 @@ "node": ">=8" } }, - "node_modules/html-tags": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", - "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", - "engines": { - "node": ">=8" + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 0.4" } }, "node_modules/icss-utils": { @@ -1242,29 +913,6 @@ "postcss": "^8.1.0" } }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "engines": { - "node": ">=4" - } - }, "node_modules/import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -1305,11 +953,6 @@ "node": ">=10.13.0" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1322,11 +965,11 @@ } }, "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1396,6 +1039,14 @@ "node": ">= 10.13.0" } }, + "node_modules/jiti": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz", + "integrity": "sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==", + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/jquery": { "version": "3.6.4", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.4.tgz", @@ -1409,11 +1060,6 @@ "node": ">=12" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -1424,17 +1070,6 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -1444,9 +1079,9 @@ } }, "node_modules/lilconfig": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", - "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", "engines": { "node": ">=10" } @@ -1475,11 +1110,6 @@ "node": ">=8" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, "node_modules/lodash.assign": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", @@ -1525,16 +1155,6 @@ "resolved": "https://registry.npmjs.org/lodash.topairs/-/lodash.topairs-4.3.0.tgz", "integrity": "sha512-qrRMbykBSEGdOgQLJJqVSdPWMD7Q+GJJ5jMRfQYb+LTLsw3tYVIabnCzRqTJb2WTo17PG5gNzXuFaZgYH/9SAQ==" }, - "node_modules/lodash.topath": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", - "integrity": "sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg==" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" - }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -1601,29 +1221,26 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/modern-normalize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.1.0.tgz", - "integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" } }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -1636,14 +1253,6 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, - "node_modules/node-emoji": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", - "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", - "dependencies": { - "lodash": "^4.17.21" - } - }, "node_modules/node-releases": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz", @@ -1657,19 +1266,18 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "peer": true, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "engines": { "node": ">=0.10.0" } }, "node_modules/object-hash": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", - "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "engines": { "node": ">= 6" } @@ -1715,34 +1323,6 @@ "node": ">=6" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -1772,14 +1352,6 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "engines": { - "node": ">=8" - } - }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -1796,6 +1368,22 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -1808,9 +1396,9 @@ } }, "node_modules/postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "funding": [ { "type": "opencollective", @@ -1819,10 +1407,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -1830,32 +1422,50 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, "node_modules/postcss-js": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-3.0.3.tgz", - "integrity": "sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", "dependencies": { - "camelcase-css": "^2.0.1", - "postcss": "^8.1.6" + "camelcase-css": "^2.0.1" }, "engines": { - "node": ">=10.0" + "node": "^12 || ^14 || >= 16" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" } }, "node_modules/postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", "dependencies": { "lilconfig": "^2.0.5", - "yaml": "^1.10.2" + "yaml": "^2.1.1" }, "engines": { - "node": ">= 10" + "node": ">= 14" }, "funding": { "type": "opencollective", @@ -1930,21 +1540,21 @@ } }, "node_modules/postcss-nested": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.5.tgz", - "integrity": "sha512-GSRXYz5bccobpTzLQZXOnSOfKl6TwVr5CyAQJUPub4nuRJSOECK5AqurxVgmtxP48p0Kc/ndY/YyS1yqldX0Ew==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", "dependencies": { - "postcss-selector-parser": "^6.0.4" + "postcss-selector-parser": "^6.0.11" }, "engines": { - "node": ">=10.0" + "node": ">=12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/postcss/" }, "peerDependencies": { - "postcss": "^8.1.13" + "postcss": "^8.2.14" } }, "node_modules/postcss-selector-parser": { @@ -1964,14 +1574,6 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, - "node_modules/pretty-hrtime": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/punycode": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.2.0.tgz", @@ -1980,28 +1582,6 @@ "node": ">=6" } }, - "node_modules/purgecss": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-4.1.3.tgz", - "integrity": "sha512-99cKy4s+VZoXnPxaoM23e5ABcP851nC2y2GROkkjS8eJaJtlciGavd7iYAw2V84WeBqggZ12l8ef44G99HmTaw==", - "dependencies": { - "commander": "^8.0.0", - "glob": "^7.1.7", - "postcss": "^8.3.5", - "postcss-selector-parser": "^6.0.6" - }, - "bin": { - "purgecss": "bin/purgecss.js" - } - }, - "node_modules/purgecss/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "engines": { - "node": ">= 12" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2021,17 +1601,6 @@ } ] }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -2040,6 +1609,14 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dependencies": { + "pify": "^2.3.0" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -2062,26 +1639,12 @@ "node": ">= 10.13.0" } }, - "node_modules/reduce-css-calc": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz", - "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==", - "dependencies": { - "css-unit-converter": "^1.1.1", - "postcss-value-parser": "^3.3.0" - } - }, - "node_modules/reduce-css-calc/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -2120,20 +1683,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -2198,9 +1747,9 @@ "integrity": "sha512-Hr9TdhyHCZUtwznEH2CBf7967mEM0idtJ5nMtjvk3Up5tPukOLXbHUNmh10oRfeNIhj+3GD3niu+g6sVK+gK0A==" }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -2249,19 +1798,6 @@ "node": ">=8" } }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2313,6 +1849,35 @@ "webpack": "^5.0.0" } }, + "node_modules/sucrase": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", + "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "engines": { + "node": ">= 6" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -2339,52 +1904,39 @@ } }, "node_modules/tailwindcss": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.2.2.tgz", - "integrity": "sha512-OzFWhlnfrO3JXZKHQiqZcb0Wwl3oJSmQ7PvT2jdIgCjV5iUoAyql9bb9ZLCSBI5TYXmawujXAoNxXVfP5Auy/Q==", - "dependencies": { - "@fullhuman/postcss-purgecss": "^4.0.3", - "arg": "^5.0.0", - "bytes": "^3.0.0", - "chalk": "^4.1.1", - "chokidar": "^3.5.1", - "color": "^3.1.3", - "cosmiconfig": "^7.0.0", - "detective": "^5.2.0", - "didyoumean": "^1.2.1", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", + "integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.2.5", - "fs-extra": "^10.0.0", - "glob-parent": "^6.0.0", - "html-tags": "^3.1.0", - "is-glob": "^4.0.1", - "lodash": "^4.17.21", - "lodash.topath": "^4.5.2", - "modern-normalize": "^1.1.0", - "node-emoji": "^1.8.1", + "fast-glob": "^3.2.12", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.18.2", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", "normalize-path": "^3.0.0", - "object-hash": "^2.2.0", - "postcss-js": "^3.0.3", - "postcss-load-config": "^3.0.1", - "postcss-nested": "5.0.5", - "postcss-selector-parser": "^6.0.6", - "postcss-value-parser": "^4.1.0", - "pretty-hrtime": "^1.0.3", - "quick-lru": "^5.1.1", - "reduce-css-calc": "^2.1.8", - "resolve": "^1.20.0", - "tmp": "^0.2.1" + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" }, "engines": { - "node": ">=12.13.0" - }, - "peerDependencies": { - "autoprefixer": "^10.0.2", - "postcss": "^8.0.9" + "node": ">=14.0.0" } }, "node_modules/tapable": { @@ -2456,15 +2008,23 @@ "node": ">=0.4.0" } }, - "node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", "dependencies": { - "rimraf": "^3.0.0" + "thenify": ">= 3.1.0 < 4" }, "engines": { - "node": ">=8.17.0" + "node": ">=0.8" } }, "node_modules/to-regex-range": { @@ -2478,13 +2038,10 @@ "node": ">=8.0" } }, - "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "engines": { - "node": ">= 10.0.0" - } + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, "node_modules/update-browserslist-db": { "version": "1.0.10", @@ -2711,25 +2268,17 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "engines": { - "node": ">=0.4" - } - }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.3.tgz", + "integrity": "sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==", "engines": { - "node": ">= 6" + "node": ">= 14" } } } diff --git a/package.json b/package.json index a372dd615e06a6bd4475e6b508f24d3e195023bb..2f24804707eab4899dae691e21d282a8b96e4172 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,15 @@ "description": "", "private": true, "dependencies": { - "@tailwindcss/typography": "^0.4.1", + "@tailwindcss/typography": "^0.5.10", "alertifyjs": "^1.13.1", "css-loader": "^6.7.3", + "easytimer.js": "^4.5.4", "jquery": "^3.6.4", "js-cookie": "^3.0.1", "select2": "^4.1.0-rc.0", "style-loader": "^3.3.2", - "tailwindcss": "^2.2.2", + "tailwindcss": "^3.3.3", "webpack": "^5.77.0", "webpack-bundle-tracker": "^1.8.1", "webpack-cli": "^5.0.1" diff --git a/requirements/base.txt b/requirements/base.txt index 287bf10146a2e892c511e89d51f443ee0c7ffce0..85f88b99360d1f80e27ae7c9065cb04e59704b87 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,3 +1,4 @@ +channels[daphne]==4.0.0 Django==4.1.5 django-database-url==1.0.3 django-environ==0.9.0 @@ -6,3 +7,4 @@ django-http-exceptions==1.4.0 gql[requests]==3.4.0 psycopg2-binary==2.9.5 requests==2.28.2 +websockets==11.0.3 diff --git a/requirements/production.txt b/requirements/production.txt index ce5169e4440b67843ee5d28199ed63e0d8323cfc..088e218c836756413516b33d6f4aaffc0971ef33 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -1,2 +1,3 @@ -gunicorn==20.1.0 whitenoise==6.3.0 +uvicorn==0.23.2 +gunicorn==21.2.0 diff --git a/run.sh b/run.sh index 5d9a7ada6bf4106d23769d523da59ae1e68e3720..402790eba0a74f9db9121b8607f176a225e70638 100644 --- a/run.sh +++ b/run.sh @@ -7,4 +7,4 @@ set -e python manage.py migrate # start webserver -exec gunicorn -c gunicorn.conf.py rybicka.wsgi +exec gunicorn -c gunicorn.conf.py rybicka.asgi:application -k uvicorn.workers.UvicornWorker diff --git a/rybicka/asgi.py b/rybicka/asgi.py index 851133e205c80ad01650d5120324f179fcb15f90..59bb6e0abd2b1719eb44521b9e544101b7c45b5e 100644 --- a/rybicka/asgi.py +++ b/rybicka/asgi.py @@ -1,16 +1,22 @@ -""" -ASGI config for rybicka project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ -""" - import os +from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.security.websocket import AllowedHostsOriginValidator from django.core.asgi import get_asgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rybicka.settings") +from timer.routing import websocket_urlpatterns + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rybicka.settings.production") +# Initialize Django ASGI application early to ensure the AppRegistry +# is populated before importing code that may import ORM models. +django_asgi_app = get_asgi_application() -application = get_asgi_application() +application = ProtocolTypeRouter( + { + "http": django_asgi_app, + "websocket": AllowedHostsOriginValidator( + AuthMiddlewareStack(URLRouter(websocket_urlpatterns)) + ), + } +) diff --git a/rybicka/settings/base.py b/rybicka/settings/base.py index fc2d8af8fd05389189842e1926b717c2e84e5e69..0fe4966f7b484930b42a8c5186c5343cf0cf4f94 100644 --- a/rybicka/settings/base.py +++ b/rybicka/settings/base.py @@ -42,8 +42,11 @@ SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # Application definition INSTALLED_APPS = [ + "daphne", + "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", + "django.contrib.sites", "django.contrib.messages", "django.contrib.staticfiles", "webpack_loader", @@ -51,6 +54,7 @@ INSTALLED_APPS = [ "member_group_size_calc", "rv_voting_calc", "mail_signature", + "timer", "asset_server_resize", ] @@ -82,7 +86,7 @@ TEMPLATES = [ }, ] -WSGI_APPLICATION = "rybicka.wsgi.application" +ASGI_APPLICATION = "rybicka.asgi.application" # Database @@ -124,3 +128,6 @@ CHOBOTNICE_API_URL = env.str( "CHOBOTNICE_API_URL", "https://chobotnice.pirati.cz/graphql/" ) CHOBOTNICE_RV_GID = env.str("CHOBOTNICE_RV_GID") + +# FIXME +CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}} diff --git a/rybicka/urls.py b/rybicka/urls.py index 1ddf91e069fa46fac44d2a45a5cdec2239df044f..634412f8340996778355ac3b9f22a420639e77af 100644 --- a/rybicka/urls.py +++ b/rybicka/urls.py @@ -19,6 +19,7 @@ urlpatterns = [ path("vypocet-skupiny-clenu/", include("member_group_size_calc.urls")), path("hlasovani-rv/", include("rv_voting_calc.urls")), path("emailove-podpisy/", include("mail_signature.urls")), + path("casovace/", include("timer.urls")), path("asset-server/", include("asset_server_resize.urls")), path("", include("shared.urls")), ] diff --git a/shared/static/shared/timer.webp b/shared/static/shared/timer.webp new file mode 100644 index 0000000000000000000000000000000000000000..9671ccf1eab3616ca75d824e4acb77ff52a58823 Binary files /dev/null and b/shared/static/shared/timer.webp differ diff --git a/shared/templates/shared/base.html b/shared/templates/shared/base.html index 47bb0c82724e93f5478c6ee079a58d462015e4a0..e0e1da9ef6ce2634bc2f0370041b5ff369c34874 100644 --- a/shared/templates/shared/base.html +++ b/shared/templates/shared/base.html @@ -45,40 +45,46 @@ {% block head %}{% endblock %} </head> <body> - <nav class="navbar navbar--simple __js-root"> - <ui-app inline-template> - <ui-navbar inline-template> - <div> - <div class="container container--default navbar__content navbar__content--initialized"> - <div class="navbar__brand flex items-center pr-8 my-4 lg:my-0"> - <a href="{% url "shared:index" %}"> - <img src="https://styleguide.pirati.cz/2.3.x/images/logo-round-white.svg" class="w-8"> - </a> - <div class="pl-4 font-bold text-xl border-r border-grey-300 pr-8"> - <a href="{% url "shared:index" %}">RybiÄka</a> + {% block nav %} + <nav class="navbar navbar--simple __js-root"> + <ui-app inline-template> + <ui-navbar inline-template> + <div> + <div class="container container--default navbar__content navbar__content--initialized"> + <div class="navbar__brand flex items-center pr-8 my-4 lg:my-0"> + <a href="{% url "shared:index" %}"> + <img src="https://styleguide.pirati.cz/2.3.x/images/logo-round-white.svg" class="w-8"> + </a> + <div class="pl-4 font-bold text-xl border-r border-grey-300 pr-8"> + <a href="{% url "shared:index" %}">RybiÄka</a> + </div> </div> + {% block header_name %}{% endblock %} </div> - {% block header_name %}{% endblock %} + </div> + </ui-navbar> + </ui-app> + </nav> + {% endblock %} + {% block raw_content %} + <div class="container container--default py-8 lg:py-24"> + {% block content %}{% endblock %} + </div> + {% endblock %} + {% block footer %} + <footer class="footer bg-grey-700 text-white __js-root hidden lg:block"> + <ui-app inline-template> + <div> + <div class="footer__main py-4 lg:py-16 container container--default"> + <section class="footer__brand"> + <p class="para text-grey-200"> + <span class="copyleft inline-block">©</span> {% now "Y" %} Piráti. VÅ¡echna práva vyhlazena. SdÃlejte a nechte ostatnà sdÃlet za stejných podmÃnek. + </p> + </section> </div> </div> - </ui-navbar> - </ui-app> - </nav> - <div class="container container--default py-8 lg:py-24"> - {% block content %}{% endblock %} - </div> - <footer class="footer bg-grey-700 text-white __js-root hidden lg:block"> - <ui-app inline-template> - <div> - <div class="footer__main py-4 lg:py-16 container container--default"> - <section class="footer__brand"> - <p class="para text-grey-200"> - <span class="copyleft inline-block">©</span> {% now "Y" %} Piráti. VÅ¡echna práva vyhlazena. SdÃlejte a nechte ostatnà sdÃlet za stejných podmÃnek. - </p> - </section> - </div> - </div> - </ui-app> - </footer> + </ui-app> + </footer> + {% endblock %} </body> </html> diff --git a/shared/templates/shared/index.html b/shared/templates/shared/index.html index 680242ceb6d3e01e6f892fd117de382fc69c626a..9c5c7e4d904b120d6d978ab6e68fef66ef5951be 100644 --- a/shared/templates/shared/index.html +++ b/shared/templates/shared/index.html @@ -77,6 +77,26 @@ </div> </li> + <li class="card"> + <a href="{% url "timer:index" %}"> + <img + src="{% static "shared/timer.webp" %}" + alt="ÄŒasovaÄ pro Å™eÄnÃky" + class="w-full h-48 object-cover" + > + </a> + <div class="p-4"> + <h2 class="mb-2 text-xl font-bold"> + <a href="{% url "timer:index" %}"> + ÄŒasovaÄ pro Å™eÄnÃky + </a> + </h2> + <div class="font-light text-sm break-words"> + VzdálenÄ› ovladatelný ÄasovaÄ pro omezenà doby projevu Å™eÄnÃků. + </div> + </div> + </li> + <li class="card"> <a href="{% url "asset_server_resize:index" %}"> <img diff --git a/static_src/member_group_size_calc.js b/static_src/member_group_size_calc.js index be1f821d036ffaaab5e52a701c4bf17e3e3774cf..8c61aef2d2b9229fd228edefe4b96fc5071f9917 100644 --- a/static_src/member_group_size_calc.js +++ b/static_src/member_group_size_calc.js @@ -1,4 +1,4 @@ -import $ from "jquery"; + import $ from "jquery"; $(window).ready( () => { diff --git a/static_src/timer.js b/static_src/timer.js new file mode 100644 index 0000000000000000000000000000000000000000..7f952babf1747f6ba6eca40b8000b39d7301553b --- /dev/null +++ b/static_src/timer.js @@ -0,0 +1,290 @@ +import $ from "jquery" +import Timer from "easytimer.js" + +import alertify from "alertifyjs"; +import "alertifyjs/build/css/alertify.css"; + +const disableInputs = () => { + $("#pause_play,#minutes,#seconds,#update_time,#reset_time").prop("disabled", true) +} + +const enableInputs = () => { + $("#pause_play,#minutes,#seconds,#update_time,#reset_time").prop("disabled", false) +} + +const updateTimeText = () => { + const timeValues = window.timer.getTimeValues() + + const hours = String(timeValues.minutes + timeValues.hours * 60 + timeValues.days * 1440).padStart(2, '0') + const seconds = String(timeValues.seconds).padStart(2, '0') + + $('#timer .timer-values').html(`${hours}:${seconds}`) +} + +const assignEventListeners = () => { + window.timer.addEventListener( + 'secondsUpdated', + (event) => { + updateTimeText() + } + ) + + window.timer.addEventListener( + 'targetAchieved', + (event) => { + window.timerIsRunning = false + $("#is_counting").prop("checked", false) + $('#timer .timer-values').html("Konec") + } + ) + + updateTimeText() +} + +const updateTimer = (data, options) => { + const timerValues = timer.getTimeValues() + const minutes = data["sync_time"]["minutes"] + const seconds = data["sync_time"]["seconds"] + + if (window.timerIsRunning && (minutes === 0 && seconds <= 5)) { + // Don't update the time if we're running in the final 5 seconds. + return + } + + if (options.minuteTolerance !== undefined && options.secondTolerance !== undefined) { + if ( + (Math.abs(timerValues.minutes - minutes) < options.minuteTolerance) + && (Math.abs(timerValues.seconds - seconds) < options.secondTolerance) + ) { + // Don't annoy the user with time changes when there is only a 1-2 second difference. + return + } else { + console.warn("Timer out of sync!") + } + } + + if (minutes === 0 && seconds === 0) { + // Let an event listener handle the timer ending. + return + } + + console.info(`Updating timer: ${minutes}:${seconds}, used to be ${timerValues.minutes}:${timerValues.seconds}`) + + window.startingTime = { + minutes: minutes, + seconds: seconds + } + + window.timer.removeAllEventListeners() + + window.timer = new Timer({ + countdown: true, + startValues: { + minutes: window.startingTime.minutes, + seconds: window.startingTime.seconds, + } + }) + + assignEventListeners() + + if (window.timerIsRunning) { + window.timer.start() + } +} + +const syncTime = (timerSocket) => { + timerSocket.send(JSON.stringify({ + "sync": window.timer.getTimeValues() + })) +} + +$(window).ready( + () => { + disableInputs() + + // --- BEGIN Timer --- + + window.timerIsRunning = false + window.timer = new Timer({ + countdown: true, + startValues: { + minutes: window.startingTime.minutes, + seconds: window.startingTime.seconds, + } + }) + + let timerSocket = null + let isInitialConnect = true + + const connectToSocket = () => { + timerSocket = new WebSocket( + ( + (window.location.protocol === "https:") ? + "wss://" : "ws://" + ) + + window.location.host + + "/ws/timer/" + + window.timerId + + "/" + ) + + if (!isInitialConnect) { + alertify.success("Obnovovánà spojenÃ.") + } + + isInitialConnect = false + + timerSocket.onmessage = (event) => { + enableInputs() + + console.info("Received timer message:", event.data) + + const data = JSON.parse(event.data) + + if ("sync_time" in data) { + updateTimer( + data, + { + minuteTolerance: 1, + secondTolerance: 2 + } + ) + } + + if ("is_running" in data) { + const remainingTime = window.timer.getTimeValues() + + if (data["is_running"]) { + // Don't do anything if we have reached an inconsistent state, + // where the timer is at 0 but server still reports it playing. + // This will be resolved within a few milliseconds. + + if (remainingTime.minutes === 0 && remainingTime.seconds === 0) { + return + } + + window.timer.start() + + $("#is_counting").prop("checked", true) + $("#pause_play > .btn__body").html("â¸ï¸Ž") + window.timerIsRunning = true + } else { + if ( + !( + window.timerIsRunning + && "sync_time" in data + && data["sync_time"]["minutes"] === 0 + && data["sync_time"]["seconds"] === 0 + ) + ) { + window.timer.pause() + window.timerIsRunning = false + } + + $("#is_counting").prop("checked", false) + $("#pause_play > .btn__body").html("âµï¸Ž") + } + } + } + + let interval = null + + timerSocket.onopen = () => { + syncTime(timerSocket) + + interval = setInterval(syncTime, 1000, timerSocket) + + // --- BEGIN Controls --- + + $("#pause_play").on( + "click", + (event) => { + if (window.timer.getTotalTimeValues().seconds === 0) { + alertify.error("ProsÃm, nastav Äas.") + + return + } + + $("#is_counting").click() + } + ) + + $("#is_counting").on( + "change", + (event) => { + disableInputs() + + if (event.target.checked) { + console.info("Starting timer") + + $("#pause_play > .btn__body").html("â¸ï¸Ž") + timerSocket.send(JSON.stringify({ + "is_running": true + })) + } else { + console.info("Stopping timer") + + $("#pause_play > .btn__body").html("âµï¸Ž") + timerSocket.send(JSON.stringify({ + "is_running": false + })) + } + } + ) + + $("#update_time").on( + "click", + (event) => { + disableInputs() + + window.timer.pause() + + let minutes = Number($("#minutes").val()) + let seconds = Number($("#seconds").val()) + + timerSocket.send(JSON.stringify({ + "time": { + "minutes": minutes, + "seconds": seconds + }, + "is_running": false + })) + + $("#is_counting").prop("checked", false) + } + ) + + $("#reset_time").on( + "click", + (event) => { + disableInputs() + + window.timer.pause() + + timerSocket.send(JSON.stringify({ + "reset": true, + "is_running": false + })) + } + ) + + // --- END Controls --- + } + + timerSocket.onclose = (event) => { + disableInputs() + + alertify.error("Ztráta spojenÃ, pokouÅ¡Ãme se o zpÄ›tné pÅ™ipojenÃ.") + + setTimeout(connectToSocket, 1000) + clearInterval(interval) + $("#is_counting,#pause_play,#update_time").unbind("click") + } + } + + connectToSocket() + assignEventListeners() + + // --- END Timer --- + } +) diff --git a/tailwind.config.js b/tailwind.config.js index 689576f4a063f1736155aff77c3d3d3f48b9738c..e2101ddc0dc9acc1cce8e705d89c4aea9d585f8f 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -3,8 +3,7 @@ const defaultTheme = require("tailwindcss/defaultTheme"); /** @type {import('tailwindcss').Config} */ module.exports = { content: [ - "*/templates/*/*.html", - "*/templates/*/*/*.html", + "*/templates/**/*.html", ], theme: { extend: { diff --git a/timer/__init__.py b/timer/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/timer/admin.py b/timer/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e --- /dev/null +++ b/timer/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/timer/apps.py b/timer/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..2760d843bca5fc9a0b893942a598eba7aeae8929 --- /dev/null +++ b/timer/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TimerConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "timer" diff --git a/timer/consumers.py b/timer/consumers.py new file mode 100644 index 0000000000000000000000000000000000000000..057d7c38c2eb1058dc1ef92f18b3098d48c16905 --- /dev/null +++ b/timer/consumers.py @@ -0,0 +1,192 @@ +import copy +import datetime +import json +import threading +import time +import timeit + +from asgiref.sync import sync_to_async +from channels.generic.websocket import AsyncWebsocketConsumer + +running_timer_threads = [] + + +def handle_expired_timer(timer) -> bool: + if timer.minutes == 0 and timer.seconds == 0: + # Stop the thread if we have reached the end + + timer.is_running = False + timer.save() + + running_timer_threads.remove(timer.id) + + return True + + return False + + +def tick_timer(timer, iteration: int, total_seconds: int) -> None: + second_compensation = 0 + + start_time = timeit.default_timer() + + timer.refresh_from_db() + + end_time = timeit.default_timer() + second_compensation = end_time - start_time + + while not timer.is_running: + time.sleep(0.05) + + timer.refresh_from_db() + + if iteration != timer.iteration: + # Stop the thread if there is a new timer + + running_timer_threads.remove(timer.id) + return + + handle_expired_timer(timer) + + start_time = timeit.default_timer() + + if iteration != timer.iteration: + # Stop the thread if there is a new timer + + running_timer_threads.remove(timer.id) + return + + total_seconds -= 1 + + seconds = total_seconds % 60 + minutes = round((total_seconds - seconds) / 60) + + timer.minutes = minutes + timer.seconds = seconds + + handle_expired_timer(timer) + + timer.save() + + end_time = timeit.default_timer() + second_compensation += end_time - start_time + + threading.Timer( + interval=max(0, 1 - second_compensation), + function=tick_timer, + args=( + timer, + iteration, + total_seconds, + ), + ).start() + + +class TimerConsumer(AsyncWebsocketConsumer): + async def connect(self) -> None: + from .models import OngoingTimer + + timer_id = self.scope["url_route"]["kwargs"]["id"] + timer = await sync_to_async(OngoingTimer.objects.filter(id=timer_id).first)() + + if timer is None: + # Not found + + await self.close() + return + + self.timer = timer + + await self.channel_layer.group_add(str(self.timer.id), self.channel_name) + await self.accept() + + if self.timer.is_running and self.timer.id not in running_timer_threads: + await self.run_timer() + + async def disconnect(self, close_code) -> None: + if hasattr(self, "timer"): + await self.channel_layer.group_discard( + str(self.timer.id), self.channel_name + ) + + async def run_timer(self, minutes: int = None, seconds: int = None) -> None: + self.timer.iteration += 1 + await sync_to_async(self.timer.save)() + + while self.timer.id in running_timer_threads: + # Wait until the thread is freed + time.sleep(0.05) + + running_timer_threads.append(self.timer.id) + + total_seconds = (self.timer.minutes * 60) + self.timer.seconds + + time.sleep(1) + + threading.Thread( + target=tick_timer, + args=( + self.timer, + self.timer.iteration, + total_seconds, + ), + ).start() + + async def receive(self, text_data: str) -> None: + await sync_to_async(self.timer.refresh_from_db)() + + json_data = json.loads(text_data) + + response = {} + + reset_timer = False + + if "is_running" in json_data: + if json_data["is_running"]: + self.timer.is_running = True + await sync_to_async(self.timer.save)() + else: + self.timer.is_running = False + await sync_to_async(self.timer.save)() + + if "time" in json_data: + # Don't save here in case there is a tick thread running + + self.timer.minutes = self.timer.initial_minutes = json_data["time"][ + "minutes" + ] + self.timer.seconds = self.timer.initial_seconds = json_data["time"][ + "seconds" + ] + + reset_timer = True + + if "reset" in json_data: + # Don't save here in case there is a tick thread running + + self.timer.minutes = self.timer.initial_minutes + self.timer.seconds = self.timer.initial_seconds + + reset_timer = True + + if reset_timer or ( + self.timer.id not in running_timer_threads and self.timer.is_running + ): + await self.run_timer() + + response.update( + { + "sync_time": { + "minutes": self.timer.minutes, + "seconds": self.timer.seconds, + }, + "is_running": self.timer.is_running, + } + ) + + await self.channel_layer.group_send( + str(self.timer.id), {"type": "timer_message", "text": json.dumps(response)} + ) + + async def timer_message(self, event: dict) -> None: + await self.send(text_data=event["text"]) diff --git a/timer/forms.py b/timer/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..435696700daf25c494908066dc3621c26456aa18 --- /dev/null +++ b/timer/forms.py @@ -0,0 +1,30 @@ +from django import forms +from django.core.validators import ( + MaxValueValidator, + MinLengthValidator, + MinValueValidator, +) + + +class TimeOnlyForm(forms.Form): + minutes = forms.IntegerField( + label="Minuty", + validators=[ + MinValueValidator(limit_value=0), + MaxValueValidator(limit_value=60), + ], + ) + + seconds = forms.IntegerField( + label="Sekundy", + validators=[ + MinValueValidator(limit_value=0), + MaxValueValidator(limit_value=60), + ], + ) + + +class NewTimerForm(TimeOnlyForm): + name = forms.CharField( + label="Název", max_length=64, validators=[MinLengthValidator(limit_value=1)] + ) diff --git a/timer/migrations/0001_initial.py b/timer/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..93e00c45a863e2f6e2f6cb051a5f591145e0c179 --- /dev/null +++ b/timer/migrations/0001_initial.py @@ -0,0 +1,57 @@ +# Generated by Django 4.1.5 on 2023-08-24 13:11 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="OngoingTimer", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=64, verbose_name="Název")), + ( + "hours", + models.IntegerField( + validators=[ + django.core.validators.MinValueValidator(limit_value=0) + ], + verbose_name="Hodiny", + ), + ), + ( + "minutes", + models.IntegerField( + validators=[ + django.core.validators.MinValueValidator(limit_value=0), + django.core.validators.MaxValueValidator(limit_value=60), + ], + verbose_name="Minuty", + ), + ), + ( + "seconds", + models.IntegerField( + validators=[ + django.core.validators.MinValueValidator(limit_value=0), + django.core.validators.MaxValueValidator(limit_value=60), + ], + verbose_name="Minuty", + ), + ), + ], + ), + ] diff --git a/timer/migrations/0002_alter_ongoingtimer_name.py b/timer/migrations/0002_alter_ongoingtimer_name.py new file mode 100644 index 0000000000000000000000000000000000000000..338b6da52b461b951a137c3f14ca0cb94135ada8 --- /dev/null +++ b/timer/migrations/0002_alter_ongoingtimer_name.py @@ -0,0 +1,22 @@ +# Generated by Django 4.1.5 on 2023-08-25 11:53 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("timer", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="ongoingtimer", + name="name", + field=models.CharField( + max_length=64, + validators=[django.core.validators.MinLengthValidator(limit_value=1)], + verbose_name="Název", + ), + ), + ] diff --git a/timer/migrations/0003_remove_ongoingtimer_hours.py b/timer/migrations/0003_remove_ongoingtimer_hours.py new file mode 100644 index 0000000000000000000000000000000000000000..d3f4014da3cdef07cfc53e68d58e9cbef631541c --- /dev/null +++ b/timer/migrations/0003_remove_ongoingtimer_hours.py @@ -0,0 +1,16 @@ +# Generated by Django 4.1.5 on 2023-08-25 16:47 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("timer", "0002_alter_ongoingtimer_name"), + ] + + operations = [ + migrations.RemoveField( + model_name="ongoingtimer", + name="hours", + ), + ] diff --git a/timer/migrations/0004_ongoingtimer_is_running_ongoingtimer_iteration.py b/timer/migrations/0004_ongoingtimer_is_running_ongoingtimer_iteration.py new file mode 100644 index 0000000000000000000000000000000000000000..ad0f3401236e0295162831d1e70eac435c8c1e09 --- /dev/null +++ b/timer/migrations/0004_ongoingtimer_is_running_ongoingtimer_iteration.py @@ -0,0 +1,22 @@ +# Generated by Django 4.1.5 on 2023-10-22 09:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("timer", "0003_remove_ongoingtimer_hours"), + ] + + operations = [ + migrations.AddField( + model_name="ongoingtimer", + name="is_running", + field=models.BooleanField(default=False, verbose_name="AktuálnÄ› běžÃ"), + ), + migrations.AddField( + model_name="ongoingtimer", + name="iteration", + field=models.IntegerField(default=0, verbose_name="Iterace"), + ), + ] diff --git a/timer/migrations/0005_ongoingtimer_initial_minutes_and_more.py b/timer/migrations/0005_ongoingtimer_initial_minutes_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..680f363fc912617040264bfd2a5d19a2945b7461 --- /dev/null +++ b/timer/migrations/0005_ongoingtimer_initial_minutes_and_more.py @@ -0,0 +1,50 @@ +# Generated by Django 4.1.5 on 2023-10-24 20:06 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("timer", "0004_ongoingtimer_is_running_ongoingtimer_iteration"), + ] + + operations = [ + migrations.AddField( + model_name="ongoingtimer", + name="initial_minutes", + field=models.IntegerField( + default=0, + validators=[ + django.core.validators.MinValueValidator(limit_value=0), + django.core.validators.MaxValueValidator(limit_value=60), + ], + verbose_name="Původnà Minuty", + ), + preserve_default=False, + ), + migrations.AddField( + model_name="ongoingtimer", + name="initial_seconds", + field=models.IntegerField( + default=0, + validators=[ + django.core.validators.MinValueValidator(limit_value=0), + django.core.validators.MaxValueValidator(limit_value=60), + ], + verbose_name="Původnà sekundy", + ), + preserve_default=False, + ), + migrations.AlterField( + model_name="ongoingtimer", + name="seconds", + field=models.IntegerField( + validators=[ + django.core.validators.MinValueValidator(limit_value=0), + django.core.validators.MaxValueValidator(limit_value=60), + ], + verbose_name="Sekundy", + ), + ), + ] diff --git a/timer/migrations/__init__.py b/timer/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/timer/models.py b/timer/models.py new file mode 100644 index 0000000000000000000000000000000000000000..a3b45aabb2a046563e2f8553411690f7907d0cc1 --- /dev/null +++ b/timer/models.py @@ -0,0 +1,52 @@ +from django.core.validators import ( + MaxValueValidator, + MinLengthValidator, + MinValueValidator, +) +from django.db import models + +# Create your models here. + + +class OngoingTimer(models.Model): + name = models.CharField( + max_length=64, + verbose_name="Název", + validators=[MinLengthValidator(limit_value=1)], + ) + + initial_minutes = models.IntegerField( + verbose_name="Původnà Minuty", + validators=[ + MinValueValidator(limit_value=0), + MaxValueValidator(limit_value=60), + ], + ) + + initial_seconds = models.IntegerField( + verbose_name="Původnà sekundy", + validators=[ + MinValueValidator(limit_value=0), + MaxValueValidator(limit_value=60), + ], + ) + + minutes = models.IntegerField( + verbose_name="Minuty", + validators=[ + MinValueValidator(limit_value=0), + MaxValueValidator(limit_value=60), + ], + ) + + seconds = models.IntegerField( + verbose_name="Sekundy", + validators=[ + MinValueValidator(limit_value=0), + MaxValueValidator(limit_value=60), + ], + ) + + is_running = models.BooleanField(verbose_name="AktuálnÄ› běžÃ", default=False) + + iteration = models.IntegerField(verbose_name="Iterace", default=0) diff --git a/timer/routing.py b/timer/routing.py new file mode 100644 index 0000000000000000000000000000000000000000..5efd46f0b127ec81064374b0768113cbab237491 --- /dev/null +++ b/timer/routing.py @@ -0,0 +1,7 @@ +from django.urls import path + +from . import consumers + +websocket_urlpatterns = [ + path("ws/timer/<int:id>/", consumers.TimerConsumer.as_asgi()), +] diff --git a/timer/templates/timer/create.html b/timer/templates/timer/create.html new file mode 100644 index 0000000000000000000000000000000000000000..4de0a7086d697a64e6bbeb2663ce4728cd86498a --- /dev/null +++ b/timer/templates/timer/create.html @@ -0,0 +1,37 @@ +{% extends "shared/base.html" %} + +{% load render_bundle from webpack_loader %} + +{% block title %}Nový ÄasovaÄ{% endblock %} +{% block header_name %}ÄŒasovaÄe{% endblock %} +{% block description %}{% endblock %} + +{% block head %} + <link + rel="stylesheet" + href="https://styleguide.pirati.cz/2.12.x/css/styles.css" + > +{% endblock %} + +{% block content %} + <main> + <h1 class="text-6xl font-bebas mb-5">Nový ÄasovaÄ</h1> + + <form + class=" + flex flex-col gap-2 + [&_label]:w-24 [&_label]:inline-block + [&_input]:bg-gray-100 [&_input]:border [&_input]:border-gray-100 [&_input]:px-2 [&_input]:py-1 + " + method="post" + > + {% csrf_token %} + + {{ form.as_div }} + + <button class="btn mt-4"> + <div class="btn__body">VytvoÅ™it</div> + </button> + </form> + </main> +{% endblock %} diff --git a/timer/templates/timer/delete_timer.html b/timer/templates/timer/delete_timer.html new file mode 100644 index 0000000000000000000000000000000000000000..62d7582ed141a4d53ab8c31f3a499700dfc22afa --- /dev/null +++ b/timer/templates/timer/delete_timer.html @@ -0,0 +1,54 @@ +{% extends "shared/base.html" %} + +{% load render_bundle from webpack_loader %} + +{% block title %}ÄŒasovaÄe{% endblock %} +{% block header_name %}ÄŒasovaÄe{% endblock %} +{% block description %}{% endblock %} + +{% block head %} + <link + rel="stylesheet" + href="https://styleguide.pirati.cz/2.12.x/css/styles.css" + > + {% render_bundle "timer" %} +{% endblock %} + +{% block content %} + <main> + <h1 class="text-6xl font-bebas">OdstranÄ›nà ÄasovaÄe {{ timer.name }}</h1> + + <div class="prose mb-4"> + SkuteÄnÄ› chceÅ¡ odstranit ÄasovaÄ {{ timer.name }}? Tento krok nelze vrátit zpÄ›t. + </div> + + <div class="flex gap-2"> + <a + class="btn btn--icon btn--black" + href="{% url 'timer:edit_timer' timer.id %}" + > + <div class="btn__body-wrap"> + <div class="btn__body">ZruÅ¡it</div> + <div class="btn__icon"> + <i class="ico--cross"></i> + </div> + </div> + </a> + + <form method="post"> + {% csrf_token %} + <button + class="btn btn--icon btn--red-600" + href="{% url 'timer:edit_timer' timer.id %}" + > + <div class="btn__body-wrap"> + <div class="btn__body">Smazat</div> + <div class="btn__icon"> + <i class="ico--checkmark"></i> + </div> + </div> + </a> + </form> + </div> + </main> +{% endblock %} diff --git a/timer/templates/timer/edit_timer.html b/timer/templates/timer/edit_timer.html new file mode 100644 index 0000000000000000000000000000000000000000..f96249971d361e269d85bf6862d7cd8c535a1ab3 --- /dev/null +++ b/timer/templates/timer/edit_timer.html @@ -0,0 +1,108 @@ +{% extends "shared/base.html" %} + +{% load render_bundle from webpack_loader %} + +{% block title %}ÄŒasovaÄe{% endblock %} +{% block header_name %}ÄŒasovaÄe{% endblock %} +{% block description %}{% endblock %} + +{% block head %} + <link + rel="stylesheet" + href="https://styleguide.pirati.cz/2.12.x/css/styles.css" + > + {% render_bundle "timer" %} +{% endblock %} + +{% block content %} + <script> + window.startingTime = { + minutes: {{ timer.minutes }}, + seconds: {{ timer.seconds }}, + } + + window.timerId = "{{ timer.id }}" + </script> + + <main> + <h1 class="text-6xl font-bebas">Úprava ÄasovaÄe {{ timer.name }}</h1> + + <a class="hover:no-underline" href="{% url 'timer:view_timer' timer.id %}"> + <i class="ico--chevron-left"></i> + <span class="underline">ZpÄ›t na zobrazenÃ</span> + </a> + + <hr> + + <div class="text-xl"> + <div id="timer"> + <div class="timer-values"></div> + </div> + </div> + + <hr> + + <div class="flex flex-col gap-5"> + <div> + <div class="hidden"> + <input + type="checkbox" + id="is_counting" + name="is_counting" + autocomplete="off" + value="false" + > + </div> + <button + id="pause_play" + class="btn w-64 text-xl" + > + <div class="btn__body">âµï¸Ž</div> + </button> + </div> + <div class="flex flex-col gap-2"> + <input + class="w-64 text-lg" + type="number" + id="minutes" + name="minutes" + min="0" + max="60" + placeholder="Minuty" + autocomplete="off" + > + <input + class="w-64 text-lg" + type="number" + id="seconds" + name="seconds" + min="0" + max="60" + placeholder="Sekundy" + autocomplete="off" + > + <button + id="update_time" + class="btn w-64" + > + <div class="btn__body">Aktualizovat Äas</div> + </button> + <button + id="reset_time" + class="btn w-64" + > + <div class="btn__body">Resetovat Äas</div> + </button> + </div> + + <a + id="delete-timer" + class="btn btn--red-600 w-64" + role="button" + href="{% url 'timer:delete_timer' timer.id %}" + > + <div class="btn__body">Smazat ÄasovaÄ</div> + </a> + </div> + </main> +{% endblock %} diff --git a/timer/templates/timer/index.html b/timer/templates/timer/index.html new file mode 100644 index 0000000000000000000000000000000000000000..dec7857d7c85e0518f325e36d96c81fd43101399 --- /dev/null +++ b/timer/templates/timer/index.html @@ -0,0 +1,42 @@ +{% extends "shared/base.html" %} + +{% load render_bundle from webpack_loader %} + +{% block title %}ÄŒasovaÄe{% endblock %} +{% block header_name %}ÄŒasovaÄe{% endblock %} +{% block description %}{% endblock %} + +{% block head %} + <link + rel="stylesheet" + href="https://styleguide.pirati.cz/2.12.x/css/styles.css" + > +{% endblock %} + +{% block content %} + <main> + <h1 class="text-6xl font-bebas mb">ÄŒasovaÄe</h1> + <h2 class="text-3xl font-bebas mb-5">Seznam aktivnÃch ÄasovaÄů</h2> + + <div class="prose max-w-none mb-8"> + <ul> + {% for timer in ongoing_timers %} + <li> + <a + href="{% url 'timer:view_timer' timer.id %}" + >{{ timer.name }}</a> + </li> + {% endfor %} + </ul> + </div> + + <a class="btn btn--icon" href="{% url 'timer:create' %}"> + <div class="btn__body-wrap"> + <div class="btn__body">Nový ÄasovaÄ</div> + <div class="btn__icon"> + <i class="ico--chevron-right"></i> + </div> + </div> + </a> + </main> +{% endblock %} diff --git a/timer/templates/timer/view_timer.html b/timer/templates/timer/view_timer.html new file mode 100644 index 0000000000000000000000000000000000000000..01ecb2197ea19df370b6bec1ccb12ccb35405419 --- /dev/null +++ b/timer/templates/timer/view_timer.html @@ -0,0 +1,54 @@ +{% extends "shared/base.html" %} + +{% load render_bundle from webpack_loader %} + +{% block title %}ÄŒasovaÄe{% endblock %} +{% block header_name %}ÄŒasovaÄe{% endblock %} +{% block description %}{% endblock %} + +{% block head %} + <link + rel="stylesheet" + href="https://styleguide.pirati.cz/2.12.x/css/styles.css" + > + {% render_bundle "timer" %} +{% endblock %} + +{% block nav %}{% endblock %} + +{% block raw_content %} + <script> + window.startingTime = { + minutes: {{ timer.minutes }}, + seconds: {{ timer.seconds }}, + } + + window.timerId = "{{ timer.id }}" + </script> + + <main class="text-center bg-black text-white h-screen flex flex-col justify-between py-5"> + <h1 class="text-6xl font-bebas mb">{{ timer.name }}</h1> + + <div + class="my-5" + style="font-size:15rem" {% comment %}TODO{% endcomment %} + > + <div id="timer"> + <div class="timer-values">{{ timer.hours }}:{{ timer.minutes }}:{{ timer.seconds }}</div> + </div> + </div> + + <div class="flex gap-4 justify-center"> + <a + class="text-white opacity-25 text-sm" + href="{% url 'timer:edit_timer' timer.id %}" + >OvládánÃ</a> + <a + class="text-white opacity-25 text-sm" + href="{% url 'timer:index' %}" + >ZpÄ›t na seznam</a> + </div> + </main> +{% endblock %} + +{% block footer %}{% endblock %} diff --git a/timer/tests.py b/timer/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/timer/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/timer/urls.py b/timer/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..a58f10cfa69ead034b2eccc39c8a87eb653f79b5 --- /dev/null +++ b/timer/urls.py @@ -0,0 +1,12 @@ +from django.urls import path + +from . import views + +app_name = "timer" +urlpatterns = [ + path("", views.index, name="index"), + path("novy", views.create, name="create"), + path("<int:id>", views.view_timer, name="view_timer"), + path("<int:id>/uprava", views.edit_timer, name="edit_timer"), + path("<int:id>/smazat", views.delete_timer, name="delete_timer"), +] diff --git a/timer/views.py b/timer/views.py new file mode 100644 index 0000000000000000000000000000000000000000..26b74f472e2794f8178f32fcef94216ba441736d --- /dev/null +++ b/timer/views.py @@ -0,0 +1,57 @@ +from django.shortcuts import get_object_or_404, redirect, render + +from .forms import NewTimerForm, TimeOnlyForm +from .models import OngoingTimer + + +def index(request): + return render( + request, + "timer/index.html", + { + "ongoing_timers": OngoingTimer.objects.all(), + }, + ) + + +def create(request): + if request.method == "POST": + form = NewTimerForm(request.POST) + + if form.is_valid(): + timer = OngoingTimer( + initial_minutes=form.cleaned_data["minutes"], + initial_seconds=form.cleaned_data["seconds"], + minutes=form.cleaned_data["minutes"], + seconds=form.cleaned_data["seconds"], + name=form.cleaned_data["name"], + ) + timer.save() + + return redirect("timer:view_timer", id=timer.id) + else: + form = NewTimerForm() + + return render(request, "timer/create.html", {"form": form}) + + +def view_timer(request, id: int): + timer = get_object_or_404(OngoingTimer, id=id) + + return render(request, "timer/view_timer.html", {"timer": timer}) + + +def edit_timer(request, id: int): + timer = get_object_or_404(OngoingTimer, id=id) + + return render(request, "timer/edit_timer.html", {"timer": timer}) + + +def delete_timer(request, id: int): + timer = get_object_or_404(OngoingTimer, id=id) + + if request.method == "POST": + timer.delete() + return redirect("timer:index") + + return render(request, "timer/delete_timer.html", {"timer": timer}) diff --git a/webpack.config.js b/webpack.config.js index 64386ccabf56b6fcb90831314851d5ee99c0b244..ce84878402ff4474e3758b8f9ef44a1e2b6c21da 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -21,6 +21,10 @@ module.exports = { import: path.resolve("static_src", "mail_signature.js"), dependOn: "shared", }, + timer: { + import: path.resolve("static_src", "timer.js"), + dependOn: "shared", + }, asset_server_resize: { import: path.resolve("static_src", "asset_server_resize.js"), dependOn: "shared", @@ -29,7 +33,7 @@ module.exports = { }, output: { path: path.resolve(__dirname, "shared", "static", "shared"), - filename: "[name]-[fullhash].js", + filename: "[name].js", }, module: { rules: [