71 Commits

Author SHA1 Message Date
ec81492d49 feat: input colors and some improvements 2025-05-06 14:48:15 +03:00
562302b5a0 hotfix: icon classname in checkbox 2025-05-06 13:18:03 +03:00
df36c180af feat: fix styles in filters 2025-05-06 13:11:57 +03:00
a53687d0f8 feat: overdue tasks 2025-05-06 12:35:35 +03:00
4d1264417e feat: cannot set previous dates 2025-05-06 11:26:58 +03:00
9e3c9ba016 feat: calendar markers on task count 2025-05-06 11:07:49 +03:00
db73f498f6 feat: style improvements 2025-05-05 15:15:25 +03:00
be7a58b93f upd: tailwind package 2025-05-05 15:15:16 +03:00
4db7276166 feat: removed btn, added icons and gradient in settings 2025-05-05 14:24:43 +03:00
ae9eef1131 fix: mobile icons in create modal 2025-05-05 14:24:23 +03:00
0b068f7232 feat: close search on x 2025-05-05 11:48:34 +03:00
5ece8ca7eb feat: search and filter 2025-04-30 16:03:05 +03:00
23c0f43526 feat: removed trash 2025-04-30 14:54:15 +03:00
5c13774e63 hotfix: mobile create button 2025-04-27 14:52:30 +03:00
5c0a7a1b5c feat: status-system 2025-04-27 14:34:10 +03:00
5086b64200 feat: tasks group by date and delete 2025-04-27 14:05:13 +03:00
a2fe9bcfd7 feat: new tasks calendar 2025-04-27 14:04:51 +03:00
ef4d1be2bc feat: working calendar 2025-04-27 13:32:17 +03:00
4ff0b441f3 feat: mobile tags adapt 2025-04-27 12:20:00 +03:00
e8ded7f2ae feat: tags on tasks 2025-04-25 15:20:20 +03:00
320872ddc9 fix: edit info reset after date change 2025-04-23 16:59:34 +03:00
9841da8b88 feat: task status change 2025-04-23 16:45:06 +03:00
b091e8d20d feat: create task form 2025-04-23 16:09:41 +03:00
c1246939cd feat: tasks editing form 2025-04-23 15:17:10 +03:00
eb12afe763 feat: fixed login 2025-04-23 13:47:16 +03:00
c629f0dcf8 feat: form hook 2025-04-23 12:32:27 +03:00
2a7d41bba5 feat: sort tasks by date 2025-04-22 12:46:45 +03:00
d7c406a930 feat: calendar on create 2025-04-22 12:39:34 +03:00
7a98dbfe91 feat: modal calendar in edit tasks 2025-04-22 12:31:55 +03:00
607637b5ca feat: modal calendar styles 2025-04-21 17:00:03 +03:00
66d7cf0532 todo: modal calendar 2025-04-21 16:37:38 +03:00
ce55ca5bfe feat: some style fixes 2025-04-21 16:37:25 +03:00
174ea24f4e feat: save tasks in localstorage 2025-04-21 16:16:52 +03:00
c4eca9b5e6 feat: add and edit task modal 2025-04-21 16:01:19 +03:00
a99be691c7 feat: primereact lib 2025-04-20 14:27:22 +03:00
bdb3effaf0 feat: calendar component 2025-04-20 14:04:39 +03:00
97879cc41b feat: fixed todos in calendar 2025-04-17 12:54:08 +03:00
5288637d19 feat: pseudo login 2025-04-16 15:00:44 +03:00
ae73b5d256 feat: input params 2025-04-16 14:33:55 +03:00
a2b1f761ac feat: some style fixes 2025-04-16 14:33:43 +03:00
a00ec135f1 feat: empty tasks page style 2025-04-16 14:13:34 +03:00
1b1c45935b feat: tasks buttons 2025-04-15 16:42:35 +03:00
9d6f0ce73d feat: tasks width 2025-04-15 15:26:37 +03:00
860b628834 feat: mobile adapt 2025-04-15 12:57:30 +03:00
af335ab4a8 feat: styles and mark tasks 2025-04-15 12:43:53 +03:00
3d4858bcbb version: v0.0.1 2025-04-15 11:26:10 +03:00
b2feaac3e1 feat: withTitle modif 2025-04-15 11:23:48 +03:00
db9132de4d feat: pseudo-auth 2025-04-14 21:10:04 +03:00
ae9bea6c7c feat: pseudo-login 2025-04-14 21:01:33 +03:00
7f80f4790d feat: input styles 2025-04-14 20:45:46 +03:00
5d11cf0dcd feat: button styles 2025-04-14 20:45:34 +03:00
3a64f039af feat: task styles 2025-04-14 20:45:18 +03:00
2e908d01f1 feat: login styles 2025-04-14 20:45:07 +03:00
9cd3acd73c feat: menu styles 2025-04-09 12:11:09 +03:00
7c6f21081f feat: 404 page 2025-04-07 17:06:08 +03:00
e6ab12f957 feat: page titles 2025-04-07 17:06:01 +03:00
a6eec16309 feat: change default path after login 2025-04-07 16:38:17 +03:00
2ef4b967f3 feat: styles update 2025-04-07 16:37:59 +03:00
083a0f27c0 feat: scrollbar styles 2025-04-07 16:06:28 +03:00
554ee083ab feat: profile styles 2025-04-07 15:59:04 +03:00
04347f4cd9 feat: logout button 2025-04-07 13:03:21 +03:00
6a6b8091c7 feat: profile tasks page 2025-04-07 13:03:11 +03:00
b9efdef024 feat: task component 2025-04-07 13:02:50 +03:00
ac9bde2a54 feat: proto-profile page 2025-04-07 12:42:40 +03:00
6c1efe702c feat: some button style changes 2025-04-07 12:42:25 +03:00
a6a145f712 feat: calendar fixes 2025-04-03 16:51:56 +03:00
bab4aa1ddb feat: calendar screen 2025-04-03 16:17:49 +03:00
48ef8c495e feat: some location optimization 2025-04-03 15:19:49 +03:00
fe9d21c218 feat: menu styles 2025-04-03 14:57:55 +03:00
454dc23245 feat: login header 2025-04-03 14:57:36 +03:00
38da06643c feat: button styles 2025-04-03 14:57:24 +03:00
39 changed files with 2597 additions and 153 deletions

3
.gitignore vendored
View File

@@ -23,3 +23,6 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# My
repomix-output.*

View File

@@ -8,5 +8,6 @@
"([\"'`][^\"'`]*.*?[\"'`])", "([\"'`][^\"'`]*.*?[\"'`])",
"[\"'`]([^\"'`]*).*?[\"'`]" "[\"'`]([^\"'`]*).*?[\"'`]"
] ]
] ],
"editor.formatOnSave": true
} }

View File

@@ -4,16 +4,22 @@
"": { "": {
"name": "anti-hvost", "name": "anti-hvost",
"dependencies": { "dependencies": {
"@heroicons/react": "^2.2.0",
"@preact/signals": "^2.0.2", "@preact/signals": "^2.0.2",
"@tailwindcss/postcss": "^4.0.17", "@tailwindcss/postcss": "^4.0.17",
"@tailwindcss/vite": "^4.0.17", "@tailwindcss/vite": "^4.0.17",
"@types/uuid": "^10.0.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"preact": "^10.26.2", "preact": "^10.26.2",
"preact-iso": "^2.9.1", "preact-iso": "^2.9.1",
"primelocale": "^2.1.2",
"primereact": "^10.9.4",
"react-hook-form": "^7.56.1",
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.0.2",
"tailwind-variants": "^1.0.0", "tailwind-variants": "^1.0.0",
"tailwindcss": "^4.0.17", "tailwindcss": "^4.1.5",
"uuid": "^11.1.0",
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.23.0", "@eslint/js": "^9.23.0",
@@ -71,6 +77,8 @@
"@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.25.9", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw=="], "@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.25.9", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw=="],
"@babel/runtime": ["@babel/runtime@7.27.0", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw=="],
"@babel/template": ["@babel/template@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA=="], "@babel/template": ["@babel/template@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA=="],
"@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="], "@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="],
@@ -147,6 +155,8 @@
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.7", "", { "dependencies": { "@eslint/core": "^0.12.0", "levn": "^0.4.1" } }, "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g=="], "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.7", "", { "dependencies": { "@eslint/core": "^0.12.0", "levn": "^0.4.1" } }, "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g=="],
"@heroicons/react": ["@heroicons/react@2.2.0", "", { "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" } }, "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
@@ -269,6 +279,12 @@
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/react": ["@types/react@19.1.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw=="],
"@types/react-transition-group": ["@types/react-transition-group@4.4.12", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="],
"@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.28.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.28.0", "@typescript-eslint/type-utils": "8.28.0", "@typescript-eslint/utils": "8.28.0", "@typescript-eslint/visitor-keys": "8.28.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.28.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.28.0", "@typescript-eslint/type-utils": "8.28.0", "@typescript-eslint/utils": "8.28.0", "@typescript-eslint/visitor-keys": "8.28.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.28.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.28.0", "@typescript-eslint/types": "8.28.0", "@typescript-eslint/typescript-estree": "8.28.0", "@typescript-eslint/visitor-keys": "8.28.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.28.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.28.0", "@typescript-eslint/types": "8.28.0", "@typescript-eslint/typescript-estree": "8.28.0", "@typescript-eslint/visitor-keys": "8.28.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ=="],
@@ -341,6 +357,8 @@
"css-what": ["css-what@6.1.0", "", {}, "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="], "css-what": ["css-what@6.1.0", "", {}, "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
@@ -353,6 +371,8 @@
"doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="],
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
@@ -517,6 +537,8 @@
"loglevel-colored-level-prefix": ["loglevel-colored-level-prefix@1.0.0", "", { "dependencies": { "chalk": "^1.1.3", "loglevel": "^1.4.1" } }, "sha512-u45Wcxxc+SdAlh4yeF/uKlC1SPUPCy0gullSNKXod5I4bmifzk+Q4lSLExNEVn19tGaJipbZ4V4jbFn79/6mVA=="], "loglevel-colored-level-prefix": ["loglevel-colored-level-prefix@1.0.0", "", { "dependencies": { "chalk": "^1.1.3", "loglevel": "^1.4.1" } }, "sha512-u45Wcxxc+SdAlh4yeF/uKlC1SPUPCy0gullSNKXod5I4bmifzk+Q4lSLExNEVn19tGaJipbZ4V4jbFn79/6mVA=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
@@ -539,6 +561,8 @@
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
@@ -579,12 +603,28 @@
"pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
"primelocale": ["primelocale@2.1.2", "", {}, "sha512-sTDkqu/QBwpqzq86oZGXaU/QbPLKLZ3Qix2gbqHXr410B7tfQCx39HSWjM9Hsnpzqn9KVdXJzk1DH1yW1iNNPg=="],
"primereact": ["primereact@10.9.4", "", { "dependencies": { "@types/react-transition-group": "^4.4.1", "react-transition-group": "^4.4.1" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-GMrelh07Wd1cwKjHpay3LCpwP346D43qBVkt8H/anGYC3z7kv5/AP0pizZv+aGQs2Fg5ufTTf+SI7IKWmyzgGg=="],
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
"react-hook-form": ["react-hook-form@7.56.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-qWAVokhSpshhcEuQDSANHx3jiAEFzu2HAaaQIzi/r9FNPm1ioAvuJSD4EuZzWd7Al7nTRKcKPnBKO7sRn+zavQ=="],
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
"react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="],
"regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
"require-relative": ["require-relative@0.8.7", "", {}, "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg=="], "require-relative": ["require-relative@0.8.7", "", {}, "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
@@ -641,6 +681,8 @@
"sass-embedded-win32-x64": ["sass-embedded-win32-x64@1.86.0", "", { "os": "win32", "cpu": "x64" }, "sha512-GVX0CHtukr3kjqfqretSlPiJzV7V4JxUjpRZV+yC9gUMTiDErilJh2Chw1r0+MYiYvumCDUSDlticmvJs7v0tA=="], "sass-embedded-win32-x64": ["sass-embedded-win32-x64@1.86.0", "", { "os": "win32", "cpu": "x64" }, "sha512-GVX0CHtukr3kjqfqretSlPiJzV7V4JxUjpRZV+yC9gUMTiDErilJh2Chw1r0+MYiYvumCDUSDlticmvJs7v0tA=="],
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
@@ -671,7 +713,7 @@
"tailwind-variants": ["tailwind-variants@1.0.0", "", { "dependencies": { "tailwind-merge": "3.0.2" }, "peerDependencies": { "tailwindcss": "*" } }, "sha512-2WSbv4ulEEyuBKomOunut65D8UZwxrHoRfYnxGcQNnHqlSCp2+B7Yz2W+yrNDrxRodOXtGD/1oCcKGNBnUqMqA=="], "tailwind-variants": ["tailwind-variants@1.0.0", "", { "dependencies": { "tailwind-merge": "3.0.2" }, "peerDependencies": { "tailwindcss": "*" } }, "sha512-2WSbv4ulEEyuBKomOunut65D8UZwxrHoRfYnxGcQNnHqlSCp2+B7Yz2W+yrNDrxRodOXtGD/1oCcKGNBnUqMqA=="],
"tailwindcss": ["tailwindcss@4.0.17", "", {}, "sha512-OErSiGzRa6rLiOvaipsDZvLMSpsBZ4ysB4f0VKGXUrjw2jfkJRd6kjRKV2+ZmTCNvwtvgdDam5D7w6WXsdLJZw=="], "tailwindcss": ["tailwindcss@4.1.5", "", {}, "sha512-nYtSPfWGDiWgCkwQG/m+aX83XCwf62sBgg3bIlNiiOcggnS1x3uVRDAuyelBFL+vJdOPPCGElxv9DjHJjRHiVA=="],
"tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="],
@@ -697,6 +739,8 @@
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
"varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="], "varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="],
"vite": ["vite@6.2.3", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg=="], "vite": ["vite@6.2.3", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg=="],
@@ -723,6 +767,12 @@
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
"@tailwindcss/node/tailwindcss": ["tailwindcss@4.0.17", "", {}, "sha512-OErSiGzRa6rLiOvaipsDZvLMSpsBZ4ysB4f0VKGXUrjw2jfkJRd6kjRKV2+ZmTCNvwtvgdDam5D7w6WXsdLJZw=="],
"@tailwindcss/postcss/tailwindcss": ["tailwindcss@4.0.17", "", {}, "sha512-OErSiGzRa6rLiOvaipsDZvLMSpsBZ4ysB4f0VKGXUrjw2jfkJRd6kjRKV2+ZmTCNvwtvgdDam5D7w6WXsdLJZw=="],
"@tailwindcss/vite/tailwindcss": ["tailwindcss@4.0.17", "", {}, "sha512-OErSiGzRa6rLiOvaipsDZvLMSpsBZ4ysB4f0VKGXUrjw2jfkJRd6kjRKV2+ZmTCNvwtvgdDam5D7w6WXsdLJZw=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
@@ -741,6 +791,8 @@
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"rollup/@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], "rollup/@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
"vue-eslint-parser/eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="], "vue-eslint-parser/eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="],

View File

@@ -1,7 +1,7 @@
{ {
"name": "anti-hvost", "name": "anti-hvost",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -9,16 +9,22 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@heroicons/react": "^2.2.0",
"@preact/signals": "^2.0.2", "@preact/signals": "^2.0.2",
"@tailwindcss/postcss": "^4.0.17", "@tailwindcss/postcss": "^4.0.17",
"@tailwindcss/vite": "^4.0.17", "@tailwindcss/vite": "^4.0.17",
"@types/uuid": "^10.0.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"preact": "^10.26.2", "preact": "^10.26.2",
"preact-iso": "^2.9.1", "preact-iso": "^2.9.1",
"primelocale": "^2.1.2",
"primereact": "^10.9.4",
"react-hook-form": "^7.56.1",
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.0.2",
"tailwind-variants": "^1.0.0", "tailwind-variants": "^1.0.0",
"tailwindcss": "^4.0.17" "tailwindcss": "^4.1.5",
"uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.23.0", "@eslint/js": "^9.23.0",

View File

@@ -1,28 +1,43 @@
import { FunctionComponent } from "preact"; import { FunctionComponent } from "preact";
import { ErrorBoundary, lazy, LocationProvider, Route, Router, useLocation } from "preact-iso"; import { ErrorBoundary, lazy, LocationProvider, Route, Router, useLocation } from "preact-iso";
import "preact/debug"; import { ru } from "primelocale/js/ru.js";
import { addLocale, locale, PrimeReactProvider } from "primereact/api";
import { useMountEffect } from "primereact/hooks";
import Page404 from "./pages/404";
import LoginPage from "./pages/login"; import LoginPage from "./pages/login";
import { AppProvider } from "./providers/AuthProvider"; import { AppProvider, useAppContext } from "./providers/AuthProvider";
const HomePage: FunctionComponent = () => { const HomePage: FunctionComponent = () => {
const location = useLocation(); const { route } = useLocation();
location.route("/login"); const { isLoggedIn } = useAppContext();
return <div>Redirecting to login...</div>; if (isLoggedIn.value) route("/profile/tasks", true);
else route("/login", true);
return <div>Redirecting...</div>;
}; };
export function App() { export function App() {
useMountEffect(() => {
addLocale("ru", ru);
locale("ru");
});
return ( return (
<AppProvider> <AppProvider>
<PrimeReactProvider
value={{
unstyled: true,
}}
>
<LocationProvider> <LocationProvider>
<ErrorBoundary> <ErrorBoundary>
<Router> <Router>
<Route path="/" component={HomePage} /> <Route path="/" component={HomePage} />
<Route path="/login" component={LoginPage} /> <Route path="/login" component={LoginPage} />
<Route path="/profile/*" component={lazy(() => import("./pages/profile"))} /> <Route path="/profile/*" component={lazy(() => import("./pages/profile"))} />
<Route default component={() => <h1>404</h1>} /> <Route default component={() => <Page404 />} />
</Router> </Router>
</ErrorBoundary> </ErrorBoundary>
</LocationProvider> </LocationProvider>
</PrimeReactProvider>
</AppProvider> </AppProvider>
); );
} }

View File

@@ -0,0 +1,219 @@
import { cn } from "@/utils/class-merge";
import { ClockIcon } from "@heroicons/react/24/outline";
import { FunctionComponent } from "preact";
import { Dispatch, StateUpdater, useEffect, useState } from "preact/hooks";
import { Calendar, CalendarPassThroughMethodOptions } from "primereact/calendar";
import { FormEvent } from "primereact/ts-helpers";
import Button from "./ui/Button";
import ModalWindow, { ModalWindowProps } from "./ui/Modal";
interface ModalCalendarProps extends ModalWindowProps {
isOpen?: boolean;
setIsOpen?: Dispatch<StateUpdater<boolean>>;
onClose?: () => void;
value?: Date;
onChange?: (e: FormEvent<Date>) => void;
}
const TRANSITIONS = {
overlay: {
timeout: 150,
cn: {
enter: "opacity-0 scale-75",
enterActive: "opacity-100 !scale-100 transition-[transform,opacity] duration-150 ease-in",
exit: "opacity-100",
exitActive: "!opacity-0 transition-opacity duration-150 ease-linear",
},
},
};
const ModalCalendar: FunctionComponent<ModalCalendarProps> = ({
isOpen,
setIsOpen,
onClose,
onChange,
value,
...rest
}) => {
const [showTime, setShowTime] = useState(false);
const [minDate, setMinDate] = useState(new Date());
useEffect(() => {
const interval = setInterval(() => {
setMinDate(new Date());
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<ModalWindow
{...rest}
isOpen={isOpen}
setIsOpen={setIsOpen}
onClose={() => {
onClose!();
setShowTime(false);
}}
className="md:h-[40rem] md:w-[30rem]"
>
<div class="w-full flex-1 self-start">
<Calendar
inline
onChange={onChange}
value={value}
hourFormat="24"
minDate={minDate}
showTime={showTime}
pt={{
root: ({ props }: CalendarPassThroughMethodOptions) => ({
className: cn("inline-flex w-full relative", {
"opacity-60 select-none pointer-events-none cursor-default": props.disabled,
}),
}),
input: ({ props }: CalendarPassThroughMethodOptions) => ({
root: {
className: cn(
"font-sans text-base text-gray-600 bg-white p-3 border border-gray-300 transition-colors duration-200 appearance-none",
"hover:border-blue-500",
{
"rounded-lg": !props.showIcon,
"border-r-0 rounded-l-lg": props.showIcon,
}
),
},
}),
dropdownButton: {
root: ({ props }: CalendarPassThroughMethodOptions) => ({
className: cn({ "rounded-l-none": props.icon }),
}),
},
panel: ({ props }: CalendarPassThroughMethodOptions) => ({
className: cn("bg-[rgba(251,194,199,0.38)]", "min-w-full", {
"shadow-md border-0 absolute": !props.inline,
"inline-block overflow-x-auto border border-gray-300 p-2 rounded-lg": props.inline,
}),
}),
header: {
className: cn(
"flex items-center justify-between",
"p-2 text-gray-700 bg-[rgba(251,194,199,0.38)] font-semibold m-0 border-b border-gray-300 rounded-t-lg"
),
},
previousButton: {
className: cn(
"flex items-center justify-center cursor-pointer overflow-hidden relative",
"w-8 h-8 text-gray-600 border-0 bg-transparent rounded-full transition-colors duration-200 ease-in-out",
"hover:text-gray-700 hover:border-transparent hover:bg-gray-200 "
),
},
title: { className: "leading-8 mx-auto" },
monthTitle: {
className: cn("text-gray-700 transition duration-200 font-semibold p-2", "mr-2", "hover:text-blue-500"),
},
yearTitle: {
className: cn("text-gray-700 transition duration-200 font-semibold p-2", "hover:text-blue-500"),
},
nextButton: {
className: cn(
"flex items-center justify-center cursor-pointer overflow-hidden relative",
"w-8 h-8 text-gray-600 border-0 bg-transparent rounded-full transition-colors duration-200 ease-in-out",
"hover:text-gray-700 hover:border-transparent hover:bg-gray-200 "
),
},
table: {
className: cn("border-collapse w-full", "my-2"),
},
tableHeaderCell: { className: "p-2" },
weekDay: { className: "text-gray-600 " },
day: { className: "p-2" },
dayLabel: ({ context }: CalendarPassThroughMethodOptions) => ({
className: cn(
"w-6 h-6 rounded-full transition-shadow duration-200 border-transparent border",
"flex items-center justify-center mx-auto overflow-hidden relative",
"focus:outline-none focus:outline-offset-0 focus:shadow-[0_0_0_0.2rem_rgba(191,219,254,1)] ",
{
"opacity-60 cursor-default": context.disabled,
"cursor-pointer": !context.disabled,
},
{
"text-gray-600 bg-transparent hover:bg-gray-200 ": !context.selected && !context.disabled,
"text-blue-700 bg-blue-100 hover:bg-blue-200": context.selected && !context.disabled,
}
),
}),
monthPicker: { className: "my-2" },
month: ({ context }: CalendarPassThroughMethodOptions) => ({
className: cn(
"w-1/3 inline-flex items-center justify-center cursor-pointer overflow-hidden relative",
"p-2 transition-shadow duration-200 rounded-lg",
"focus:outline-none focus:outline-offset-0 focus:shadow-[0_0_0_0.2rem_rgba(191,219,254,1)] ",
{
"text-gray-600 bg-transparent hover:bg-gray-200 ": !context.selected && !context.disabled,
"text-blue-700 bg-blue-100 hover:bg-blue-200": context.selected && !context.disabled,
}
),
}),
yearPicker: {
className: cn("my-2"),
},
year: ({ context }: CalendarPassThroughMethodOptions) => ({
className: cn(
"w-1/2 inline-flex items-center justify-center cursor-pointer overflow-hidden relative",
"p-2 transition-shadow duration-200 rounded-lg",
"focus:outline-none focus:outline-offset-0 focus:shadow-[0_0_0_0.2rem_rgba(191,219,254,1)] ",
{
"text-gray-600 bg-transparent hover:bg-gray-200 ": !context.selected && !context.disabled,
"text-blue-700 bg-blue-100 hover:bg-blue-200": context.selected && !context.disabled,
}
),
}),
timePicker: {
className: cn("flex justify-center items-center", "border-t-1 border-solid border-gray-300"),
},
separatorContainer: { className: "flex items-center flex-col px-2" },
separator: { className: "text-xl" },
hourPicker: { className: "flex items-center flex-col px-2" },
minutePicker: { className: "flex items-center flex-col px-2" },
ampmPicker: { className: "flex items-center flex-col px-2" },
incrementButton: {
className: cn(
"flex items-center justify-center cursor-pointer overflow-hidden relative",
"w-8 h-8 text-gray-600 border-0 bg-transparent rounded-full transition-colors duration-200 ease-in-out",
"hover:text-gray-700 hover:border-transparent hover:bg-gray-200 "
),
},
decrementButton: {
className: cn(
"flex items-center justify-center cursor-pointer overflow-hidden relative",
"w-8 h-8 text-gray-600 border-0 bg-transparent rounded-full transition-colors duration-200 ease-in-out",
"hover:text-gray-700 hover:border-transparent hover:bg-gray-200 "
),
},
groupContainer: { className: "flex" },
group: {
className: cn(
"flex-1",
"border-l border-gray-300 pr-0.5 pl-0.5 pt-0 pb-0",
"first:pl-0 first:border-l-0"
),
},
transition: TRANSITIONS.overlay,
}}
/>
<p class={cn("mt-2 text-center text-xl font-semibold", { hidden: !value })}>
{value && Intl.DateTimeFormat("ru-RU", { day: "2-digit", month: "2-digit", year: "numeric" }).format(value)}
</p>
</div>
<Button
className="flex w-full flex-row items-center justify-center gap-2 self-end"
onClick={() => {
setShowTime(!showTime);
}}
>
<ClockIcon class="h-8" /> <p>Время</p>
</Button>
</ModalWindow>
);
};
ModalCalendar.displayName = "AHModalCalendar";
export default ModalCalendar;

View File

@@ -0,0 +1,135 @@
import { cn } from "@/utils/class-merge";
import { BookOpenIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline";
import { FunctionComponent } from "preact";
import { Dispatch, StateUpdater, useEffect, useState } from "preact/hooks";
import Button from "./ui/Button";
import ModalWindow, { ModalWindowProps } from "./ui/Modal";
export interface ITags {
first: string;
second: string;
overdue: boolean;
}
interface ModalTagsProps extends ModalWindowProps {
isOpen?: boolean;
setIsOpen?: Dispatch<StateUpdater<boolean>>;
onClose?: () => void;
value?: ITags;
onChange?: Dispatch<StateUpdater<ITags>>;
tagsList?: {
first: string[];
second: string[];
};
}
const ModalTags: FunctionComponent<ModalTagsProps> = ({
isOpen,
setIsOpen,
onClose,
onChange,
value,
tagsList,
...rest
}) => {
const [showFirstTags, setShowFirstTags] = useState(false);
const [showSecondTags, setShowSecondTags] = useState(false);
useEffect(() => {
if (showFirstTags && showSecondTags) setShowFirstTags(false);
}, [showSecondTags]);
useEffect(() => {
if (showFirstTags && showSecondTags) setShowSecondTags(false);
}, [showFirstTags]);
return (
<ModalWindow
isOpen={isOpen}
{...rest}
setIsOpen={setIsOpen}
onClose={() => {
onClose!();
setShowFirstTags(false);
setShowSecondTags(false);
}}
className="relative h-[14rem] justify-between py-4 md:h-[14rem] md:w-[25rem]"
zIndex={70}
>
<p class="text-2xl font-semibold">Теги</p>
<div class="flex w-[85%] flex-col gap-2 md:w-full">
<Button
className="flex w-full flex-row items-center justify-center gap-1 text-[1rem]!"
onClick={() => setShowFirstTags(!showFirstTags)}
>
<BookOpenIcon class="size-6" />
{value?.first || "Предмет"}
</Button>
<Button
className="flex w-full flex-row items-center justify-center gap-1 text-[1rem]! break-words"
onClick={() => setShowSecondTags(!showSecondTags)}
>
<DocumentDuplicateIcon class="size-6" />
{value?.second || "Тема"}
</Button>
{showFirstTags && (
<div class="absolute top-full left-0 mt-3 ml-2 flex h-fit w-[18rem] flex-col items-center rounded-[4rem] bg-white px-8 py-4 text-xs md:top-0 md:left-full md:mt-0 md:ml-5">
<div class="flex h-fit max-h-[45vh] w-full flex-col gap-3 overflow-y-auto">
{tagsList?.first.map((tag) => (
<div
class="flex cursor-pointer flex-row gap-2"
onClick={() =>
onChange?.((prev) => {
return { ...prev, first: tag };
})
}
>
<div
class={cn(
"pointer-events-none flex aspect-square size-4 flex-col items-center justify-center rounded-full border-1 border-black text-white select-none",
{
"bg-black": value?.first === tag,
}
)}
>
</div>
<p>{tag}</p>
</div>
))}
</div>
</div>
)}
{showSecondTags && (
<div class="absolute top-full left-0 mt-3 ml-2 flex h-fit w-[18rem] flex-col items-center rounded-[4rem] bg-white px-8 py-4 text-xs md:top-0 md:left-full md:mt-0 md:ml-5">
<div class="flex h-fit max-h-[45vh] w-full flex-col gap-3 overflow-y-auto">
{tagsList?.second.map((tag) => (
<div
class="flex cursor-pointer flex-row gap-2"
onClick={() =>
onChange?.((prev) => {
return { ...prev, second: tag };
})
}
>
<div
class={cn(
"pointer-events-none flex aspect-square size-4 flex-col items-center justify-center rounded-full border-1 border-black text-white select-none",
{
"bg-black": value?.second === tag,
}
)}
>
</div>
<p>{tag}</p>
</div>
))}
</div>
</div>
)}
</div>
</ModalWindow>
);
};
ModalTags.displayName = "AHModalTags";
export default ModalTags;

View File

@@ -0,0 +1 @@
@reference "../index.scss";

270
src/components/calendar.tsx Normal file
View File

@@ -0,0 +1,270 @@
import { cn } from "@/utils/class-merge";
import { BackwardIcon } from "@heroicons/react/24/solid";
import { FunctionComponent, h } from "preact";
import { useState } from "preact/hooks";
interface BackButtonProps {
selectedDate: Date;
onClick: () => void;
}
const BackButton: FunctionComponent<BackButtonProps> = ({ selectedDate, onClick }: BackButtonProps) => {
const currentDate = new Date();
return currentDate.getMonth() !== selectedDate.getMonth() ? (
<div
class="fixed bottom-4 left-8 hidden aspect-square h-24 cursor-pointer flex-col items-center justify-center rounded-full bg-[rgb(251,194,199,0.53)] shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] hover:bg-[rgb(251,194,199,0.7)] md:flex"
onClick={onClick}
>
<BackwardIcon class="size-8 text-sm text-white" />
</div>
) : null;
};
type MarkedDateType = "event" | "holiday" | "important" | string;
type MarkedDates = Record<string, MarkedDateType>;
interface BigCalendarProps {
onDateSelect?: (date: Date) => void;
markedDates?: MarkedDates;
className?: string;
}
const BigCalendar: FunctionComponent<BigCalendarProps> = ({
onDateSelect = () => {},
markedDates = {},
className = "",
}: BigCalendarProps) => {
const [currentDate, setCurrentDate] = useState<Date>(new Date());
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
const monthNames: string[] = [
"Январь",
"Февраль",
"Март",
"Апрель",
"Май",
"Июнь",
"Июль",
"Август",
"Сентябрь",
"Октябрь",
"Ноябрь",
"Декабрь",
];
const dayNames: string[] = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"];
const getDaysInMonth = (year: number, month: number): number => {
return new Date(year, month + 1, 0).getDate();
};
const getFirstDayOfMonth = (year: number, month: number): number => {
const day = new Date(year, month, 1).getDay();
return day === 0 ? 6 : day;
};
const handlePrevMonth = (): void => {
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1));
};
const handleNextMonth = (): void => {
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1));
};
const handleDateClick = (date: Date, isCurrentMonth: boolean): void => {
if (!isCurrentMonth) {
setCurrentDate(new Date(date.getFullYear(), date.getMonth(), 1));
}
setSelectedDate(date);
onDateSelect(date);
};
const isDateMarked = (date: Date): MarkedDateType | undefined => {
const dateStr = date.toISOString().split("T")[0];
return markedDates[dateStr];
};
const getLastDayOfMonth = (year: number, month: number): number => {
const day = new Date(year, month + 1, 0).getDay();
return day === 0 ? 6 : day;
};
const renderDays = (): h.JSX.Element[] => {
const year: number = currentDate.getFullYear();
const month: number = currentDate.getMonth();
const daysInMonth: number = getDaysInMonth(year, month);
const firstDayOfMonth: number = getFirstDayOfMonth(year, month);
const lastDayOfMonth: number = getLastDayOfMonth(year, month);
const days: h.JSX.Element[] = [];
// Дни предыдущего месяца
const prevMonthDays = getDaysInMonth(year, month - 1);
const daysFromPrevMonth = firstDayOfMonth === 0 ? 6 : firstDayOfMonth;
for (let i = daysFromPrevMonth - 1; i >= 0; i--) {
const day = prevMonthDays - i;
const date = new Date(year, month - 1, day);
const dateStr = date.toISOString().split("T")[0];
const isSelected = selectedDate?.toISOString().split("T")[0] === dateStr;
const markType = isDateMarked(date);
days.push(
<div
key={`prev-${day}`}
className={cn(
"relative flex h-24 cursor-pointer flex-col border border-gray-200 p-2 opacity-50 hover:opacity-70",
{ "bg-gray-200": isSelected }
)}
onClick={() => handleDateClick(date, false)}
>
<div className="flex h-8 w-8 items-center justify-center self-end rounded-full text-gray-600">{day}</div>
{markType && (
<div
className={cn(
"mb-1 truncate rounded p-1 text-xs",
{ "bg-green-100 text-green-800": markType === "event" },
{ "bg-red-100 text-red-800": markType === "holiday" },
{ "bg-yellow-100 text-yellow-800": markType === "important" }
)}
>
{markType}
</div>
)}
</div>
);
}
// Дни текущего месяца
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(year, month, day);
const dateStr = date.toISOString().split("T")[0];
const isSelected = selectedDate?.toISOString().split("T")[0] === dateStr;
const markType = isDateMarked(date);
const isToday = new Date().toISOString().split("T")[0] === dateStr;
days.push(
<div
key={`current-${day}`}
className={cn(
"relative flex h-24 flex-col border border-gray-200 p-2",
{ "border-blue-400 bg-blue-100": isSelected },
{ "border-yellow-400": isToday },
"cursor-pointer hover:bg-gray-50"
)}
onClick={() => (selectedDate ? setSelectedDate(null) : handleDateClick(date, true))}
>
<div
className={cn(
"flex h-8 w-8 items-center justify-center self-end rounded-full",
{ "bg-blue-600 text-white": isSelected },
{ "bg-yellow-100 text-yellow-800": isToday && !isSelected }
)}
>
{day}
</div>
{markType && (
<div
className={cn(
"mb-1 truncate rounded p-1 text-xs",
{ "bg-green-100 text-green-800": markType === "event" },
{ "bg-red-100 text-red-800": markType === "holiday" },
{ "bg-yellow-100 text-yellow-800": markType === "important" }
)}
>
{markType}
</div>
)}
</div>
);
}
// Дни следующего месяца
const daysToAdd = 6 - (lastDayOfMonth === 6 ? 3 : lastDayOfMonth);
for (let day = 1; day <= daysToAdd; day++) {
const date = new Date(year, month + 1, day);
const dateStr = date.toISOString().split("T")[0];
const isSelected = selectedDate?.toISOString().split("T")[0] === dateStr;
const markType = isDateMarked(date);
days.push(
<div
key={`next-${day}`}
className={cn(
"relative flex h-24 cursor-pointer flex-col border border-gray-200 p-2 opacity-50 hover:opacity-70",
{ "bg-gray-200": isSelected }
)}
onClick={() => handleDateClick(date, false)}
>
<div className="flex h-8 w-8 items-center justify-center self-end rounded-full text-gray-600">{day}</div>
{markType && (
<div
className={cn(
"mb-1 truncate rounded p-1 text-xs",
{ "bg-green-100 text-green-800": markType === "event" },
{ "bg-red-100 text-red-800": markType === "holiday" },
{ "bg-yellow-100 text-yellow-800": markType === "important" }
)}
>
{markType}
</div>
)}
</div>
);
}
return days;
};
return (
<div className={`flex w-full flex-col ${className}`}>
<div class="fixed top-0 right-[20rem] z-100 flex h-full w-full flex-col items-center justify-center bg-white">
Закрыто
</div>
<div className="mb-4 flex items-center justify-between px-2">
<button
onClick={handlePrevMonth}
className="rounded-lg p-2 text-gray-700 hover:bg-gray-100"
aria-label="Previous month"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</button>
<h2 className="text-xl font-bold text-gray-800">
{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}
</h2>
<button
onClick={handleNextMonth}
className="rounded-lg p-2 text-gray-700 hover:bg-gray-100"
aria-label="Next month"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
<div className="mb-2 grid grid-cols-7 gap-1 text-center text-sm font-medium text-gray-500">
{dayNames.map((day) => (
<div key={day} className="flex h-10 items-center justify-center">
{day}
</div>
))}
</div>
<div className="grid flex-1 grid-cols-7 gap-1">{renderDays()}</div>
<BackButton selectedDate={currentDate} onClick={() => setCurrentDate(new Date())} />
</div>
);
};
export default BigCalendar;

View File

@@ -1,9 +1,13 @@
@reference "../index.scss"; @reference "../index.scss";
#container {
@apply flex min-h-auto w-screen flex-col items-center gap-4 px-2 py-3 md:min-h-screen md:w-[20rem];
}
.menu_container { .menu_container {
@apply fixed bottom-0 flex min-h-8 min-w-screen flex-row items-center gap-1 border-t border-t-gray-500 bg-white px-1 py-5 md:relative md:min-h-screen md:min-w-[300px] md:flex-col md:gap-4 md:border-t-0 md:border-l md:border-l-gray-500 md:px-5; @apply fixed right-0 bottom-0 left-0 flex h-[4rem] w-full flex-1 flex-row items-center gap-1 rounded-[3rem] bg-[rgba(167,213,246,0.3)] px-1 py-5 md:sticky md:top-0 md:right-0 md:bottom-auto md:left-auto md:h-full md:flex-col md:gap-4 md:border-t-0 md:px-5 md:py-5;
} }
.menu_item { .menu_item {
@apply w-full cursor-pointer rounded-md px-3 py-2 text-center hover:bg-gray-200 md:px-6; @apply flex w-full flex-1/3 cursor-pointer flex-row items-center justify-center gap-3 rounded-full px-3 py-2 text-center hover:bg-gray-200 md:flex-initial md:justify-start md:px-6;
} }

View File

@@ -1,22 +1,27 @@
import { FunctionComponent } from "preact"; import { cn } from "@/utils/class-merge";
import { CalendarDaysIcon, ListBulletIcon, UserIcon } from "@heroicons/react/24/solid";
import { FunctionComponent, h } from "preact";
import { useLocation } from "preact-iso"; import { useLocation } from "preact-iso";
import { tv } from "tailwind-variants"; import { tv } from "tailwind-variants";
import classes from "./menu.module.scss"; import classes from "./menu.module.scss";
import { calculatePoints, getCurrentStatus } from "@/utils/status-system";
import { useEffect, useState } from "preact/hooks";
interface MenuItemProps { interface MenuItemProps {
title: string; title: string;
link: string; link: string;
icon: h.JSX.Element;
} }
const MenuItem: FunctionComponent<MenuItemProps> = ({ title, link }: MenuItemProps) => { const MenuItem: FunctionComponent<MenuItemProps> = ({ title, link, icon }: MenuItemProps) => {
const location = useLocation(); const { route, path } = useLocation();
const active = location.path === link; const active = path === link;
const menuItemClasses = tv({ const menuItemClasses = tv({
base: classes.menu_item, base: classes.menu_item,
variants: { variants: {
activity: { activity: {
active: "bg-gray-200", active: "bg-gray-200",
inactive: "bg-gray-100", inactive: "bg-white",
}, },
}, },
defaultVariants: { defaultVariants: {
@@ -24,38 +29,96 @@ const MenuItem: FunctionComponent<MenuItemProps> = ({ title, link }: MenuItemPro
}, },
}); });
return ( return (
<div class={menuItemClasses({ activity: active ? "active" : "inactive" })} onClick={() => location.route(link)}> <div class={menuItemClasses({ activity: active ? "active" : "inactive" })} onClick={() => route(link, true)}>
{title} {icon}
<div class="hidden md:block">{title}</div>
</div> </div>
); );
}; };
const Avatar: FunctionComponent = () => {
const [status, setStatus] = useState("");
const { route, path } = useLocation();
useEffect(() => {
const updateStatus = () => {
const tasks = JSON.parse(localStorage.getItem("tasks") || "[]");
const completedTasks = tasks.filter((task: { checked: boolean }) => task.checked).length;
const points = calculatePoints(completedTasks);
setStatus(getCurrentStatus(points));
};
// Initial update
updateStatus();
// Update when tasks change
const handleStorage = (e: StorageEvent) => {
if (e.key === "tasks") {
updateStatus();
}
};
window.addEventListener('storage', handleStorage);
return () => window.removeEventListener('storage', handleStorage);
}, []);
return (
<button
onClick={() => route("/profile/settings")}
class={cn("hidden h-32 w-full cursor-pointer overflow-hidden opacity-100 transition-[height,opacity] md:block", {
"h-0 opacity-0": path === "/profile/settings",
})}
>
<div
class={cn(
"h-full flex-row items-center justify-around rounded-[3rem] bg-[linear-gradient(180.00deg,rgba(249,134,143,0.5)_3.053%,rgb(228,242,252)_96.183%)] px-5 py-5 md:flex"
)}
>
<div class="my-5 aspect-square h-full rounded-full bg-white"></div>
<div class="flex flex-col items-center justify-center">
<p class="text-3xl font-semibold">никнейм</p>
<div class="rounded-[1rem] bg-white px-5 leading-5 font-light italic">{status}</div>
</div>
</div>
</button>
);
};
interface MenuItems { interface MenuItems {
title: string; title: string;
link: string; link: string;
icon: h.JSX.Element;
} }
const Menu: FunctionComponent = () => { const Menu: FunctionComponent = () => {
//TODO: Move links to enum
const menu_items: MenuItems[] = [ const menu_items: MenuItems[] = [
{
title: "Профиль",
link: "/profile/settings",
},
{ {
title: "Задачи", title: "Задачи",
link: "/profile/tasks", link: "/profile/tasks",
icon: <ListBulletIcon class="size-10" />,
}, },
{ {
title: "Календарь", title: "Календарь",
link: "/profile/calendar", link: "/profile/calendar",
icon: <CalendarDaysIcon class="size-10" />,
},
{
title: "Профиль",
link: "/profile/settings",
icon: <UserIcon class="size-10" />,
}, },
]; ];
return ( return (
<div id={classes.container}>
<Avatar />
<div class={classes.menu_container}> <div class={classes.menu_container}>
{menu_items.map(({ title, link }) => ( {menu_items.map(({ title, link, icon }) => (
<MenuItem title={title} link={link} /> <MenuItem title={title} link={link} icon={icon} />
))} ))}
</div> </div>
</div>
); );
}; };

View File

@@ -0,0 +1,5 @@
@reference "../index.scss";
.task {
@apply relative flex h-24 w-full cursor-pointer flex-row items-center justify-start gap-4 rounded-[3rem] bg-[rgba(251,194,199,0.53)] px-5 py-6 text-xl shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] transition-transform hover:scale-[1.05] hover:bg-[rgba(251,194,199,0.7)] active:scale-[1.05] md:w-[500px];
}

62
src/components/task.tsx Normal file
View File

@@ -0,0 +1,62 @@
import { FunctionComponent } from "preact";
import { MouseEventHandler } from "preact/compat";
import { tv } from "tailwind-variants";
import classes from "./task.module.scss";
interface TaskProps {
name: string;
checked?: boolean;
overdue?: boolean;
onClick?: () => void;
onMarkClick?: MouseEventHandler<HTMLParagraphElement>;
}
const taskStyle = tv({
base: "hover:scale-[1.1]active:scale-[1.1] flex aspect-square h-full flex-col items-center justify-center rounded-full border transition-[scale] duration-100 ease-in-out select-none",
variants: {
checked: {
true: "bg-black",
false: "bg-white",
},
},
});
const markStyle = tv({
base: "text-4xl font-light text-white",
variants: {
checked: {
true: "block",
false: "hidden",
},
},
});
const Task: FunctionComponent<TaskProps> = ({
name,
checked = false,
onClick = () => {},
onMarkClick = () => {},
overdue,
}) => {
return (
<div class="w-[95%]">
<div class={classes.task} onClick={onClick}>
<div
class={taskStyle({ checked })}
onClick={(e) => {
e.stopPropagation();
onMarkClick(e);
}}
>
<p class={markStyle({ checked })}></p>
</div>
{name}
{overdue && <span class="absolute top-2 right-16 text-xs text-red-500">Просрочено</span>}
</div>
</div>
);
};
Task.displayName = "AHTask";
export default Task;

View File

@@ -1,5 +1,5 @@
@reference '../../index.scss'; @reference '../../index.scss';
.button { .button {
@apply rounded-2xl border-2 py-3 font-semibold text-white transition-colors hover:cursor-pointer; @apply rounded-4xl px-4 py-3 text-xl text-black shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] transition-colors hover:cursor-pointer focus:outline-1;
} }

View File

@@ -1,22 +1,31 @@
import { FunctionComponent } from "preact"; import { forwardRef, HTMLProps } from "preact/compat";
import { tv } from "tailwind-variants"; import { tv } from "tailwind-variants";
import classes from "./Button.module.scss"; import classes from "./Button.module.scss";
const button = tv({ const button = tv({
base: classes.button, base: classes.button,
variants: { variants: {
color: { color: {
primary: "bg-blue-400 hover:bg-blue-500", primary: "bg-[rgba(206,232,251,0.7)] hover:bg-[rgba(206,232,251,0.9)] active:bg-[rgba(206,232,251,0.9)]",
secondary: "bg-red-400 hover:bg-red-500", secondary: "bg-[rgba(255,251,197,0.68)] hover:bg-[rgba(255,251,197,0.9)] active:bg-[rgba(255,251,197,0.9)]",
red: "bg-[rgba(251,194,199,0.53)] hover:bg-[rgba(251,194,199,0.9)] active:bg-[rgba(251,194,199,0.9)]",
}, },
}, },
}); });
const Button: FunctionComponent<{ onClick: () => void }> = (props) => { interface ButtonProps extends HTMLProps<HTMLButtonElement> {
color?: "primary" | "secondary" | "red";
className?: string;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
const { children, onClick = () => {}, color = "primary", className = "", type = "button" } = props;
return ( return (
<button type="button" class={button({ color: "primary" })} onClick={props.onClick}> <button ref={ref} type={type} class={button({ color: color, class: className })} onClick={onClick}>
{props.children} {children}
</button> </button>
); );
}; });
Button.displayName = "AHButton";
export default Button; export default Button;

View File

@@ -0,0 +1,14 @@
import { FunctionComponent } from "preact";
import { Calendar } from "primereact/calendar";
import { FormEvent } from "primereact/ts-helpers";
interface DatePickerProps {
value?: Date;
onChange?: (e: FormEvent<Date>) => void;
}
const DatePicker: FunctionComponent<DatePickerProps> = ({ value = new Date(), onChange }: DatePickerProps) => {
return <Calendar value={value} onChange={onChange} />;
};
export default DatePicker;

View File

@@ -0,0 +1,49 @@
import { FunctionComponent } from "preact";
import Button from "./Button";
interface DialogProps {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
title: string;
content: string;
onConfirm: () => void;
confirmText?: string;
cancelText?: string;
}
const Dialog: FunctionComponent<DialogProps> = ({
isOpen,
setIsOpen,
title,
content,
onConfirm,
confirmText = "Подтвердить",
cancelText = "Отмена",
}) => {
if (!isOpen) return null;
return (
<div class="fixed inset-0 z-150 flex items-center justify-center bg-black/50">
<div class="w-full max-w-md rounded-[4rem] bg-white p-6 shadow-lg">
<h2 class="mb-4 text-xl font-semibold">{title}</h2>
<p class="mb-6 text-sm sm:text-[1rem]">{content}</p>
<div class="flex justify-end gap-4">
<Button onClick={() => setIsOpen(false)} className="bg-gray-200 text-gray-800 hover:bg-gray-300">
{cancelText}
</Button>
<Button
onClick={() => {
onConfirm();
setIsOpen(false);
}}
color="red"
>
{confirmText}
</Button>
</div>
</div>
</div>
);
};
export default Dialog;

View File

@@ -1,5 +1,5 @@
@reference "../../index.scss"; @reference "../../index.scss";
.input_field { .input_field {
@apply rounded-md border border-gray-300 p-2 placeholder:transition focus:outline-0 focus:placeholder:opacity-25; @apply w-full rounded-[4rem] border bg-white p-2 leading-8 placeholder:transition focus:outline-0 focus:placeholder:opacity-25;
} }

View File

@@ -1,4 +1,5 @@
import { FunctionComponent } from "preact"; import { cn } from "@/utils/class-merge";
import { forwardRef, HTMLProps, useEffect } from "preact/compat";
import { tv } from "tailwind-variants"; import { tv } from "tailwind-variants";
import classes from "./Input.module.scss"; import classes from "./Input.module.scss";
@@ -9,26 +10,40 @@ const input = tv({
center: "text-center", center: "text-center",
left: "text-left", left: "text-left",
}, },
"border-error": {
true: "border-red-500 placeholder:text-red-500",
false: "border-gray-300",
},
}, },
defaultVariants: { defaultVariants: {
"text-align": "left", "text-align": "left",
}, },
}); });
interface InputProps { interface InputProps extends HTMLProps<HTMLInputElement> {
isPassword?: boolean;
placeholder?: string;
textAlign?: "center" | "left"; textAlign?: "center" | "left";
error?: string;
} }
const Input: FunctionComponent<InputProps> = ({ isPassword = false, placeholder = "", textAlign }: InputProps) => { const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
const { textAlign, error, type = "text", ...rest } = props;
useEffect(() => {
console.log(`error: ${error}`);
}, [error]);
return ( return (
<input <div class="flex w-full flex-col items-center gap-1">
type={isPassword ? "password" : "text"} <input class={input({ "text-align": textAlign, "border-error": !!error })} ref={ref} type={type} {...rest} />
class={input({ "text-align": textAlign })} <p
placeholder={placeholder} class={cn("invisible h-10 w-[80%] text-center text-[0.7rem] break-words text-red-500", {
/> visible: error !== "",
})}
>
{error}
</p>
</div>
); );
}; });
Input.displayName = "AHInput";
export default Input; export default Input;

View File

@@ -0,0 +1,50 @@
import { cn } from "@/utils/class-merge";
import { FunctionComponent } from "preact";
import { Dispatch, StateUpdater, useEffect } from "preact/hooks";
export interface ModalWindowProps {
isOpen?: boolean;
setIsOpen?: Dispatch<StateUpdater<boolean>>;
onClose?: () => void;
className?: string;
zIndex?: number;
}
const ModalWindow: FunctionComponent<ModalWindowProps> = ({
isOpen,
children,
setIsOpen,
onClose,
className = "",
zIndex,
}) => {
useEffect(() => {
if (isOpen) return;
if (onClose) onClose();
}, [isOpen]);
return (
isOpen && (
<div
onClick={(e) => {
e.stopPropagation();
if (setIsOpen) setIsOpen(false);
}}
class={cn(
"fixed top-0 left-0 z-50 flex h-screen w-screen cursor-pointer flex-col items-center justify-center bg-black/50"
)}
style={{ zIndex: zIndex }}
>
<div
class={cn(
"flex h-[40rem] w-[95%] cursor-auto flex-col items-center justify-start rounded-[4rem] bg-white px-8 py-12 md:me-[20rem] md:h-[20rem] md:w-[65%] md:px-16",
className
)}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>
)
);
};
export default ModalWindow;

View File

@@ -0,0 +1,12 @@
import { FunctionComponent } from "preact";
import { useEffect } from "preact/hooks";
export const withTitle = <P,>(title: string, WrappedComponent: FunctionComponent<P>): FunctionComponent<P> => {
const ComponentWithTitle: FunctionComponent<P> = (props) => {
useEffect(() => {
document.title = title;
}, []);
return <WrappedComponent {...props} />;
};
return ComponentWithTitle;
};

7
src/enums/urls.ts Normal file
View File

@@ -0,0 +1,7 @@
export enum UrlsTitle {
LOGIN = "Авторизация",
PROFILE = "Профиль",
TASKS = "Задачи",
CALENDAR = "Календарь",
PAGE404 = "404",
}

View File

@@ -1,5 +1,23 @@
@import "tailwindcss"; @import "tailwindcss";
@import url("https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap");
:root { :root {
color: black;
background-color: white;
font-family: "Montserrat", sans-serif; font-family: "Montserrat", sans-serif;
} }
@layer base {
::-webkit-scrollbar {
@apply w-2;
}
::-webkit-scrollbar-track {
@apply bg-gray-100;
}
::-webkit-scrollbar-thumb {
@apply rounded-full bg-gray-300;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-gray-500;
}
}

View File

@@ -0,0 +1,9 @@
@reference "../index.scss";
#container {
@apply flex h-screen w-full flex-col items-center justify-center;
}
#main_container {
@apply flex flex-col items-center gap-8;
}

27
src/pages/404.tsx Normal file
View File

@@ -0,0 +1,27 @@
import Button from "@/components/ui/Button";
import { withTitle } from "@/constructors/Component";
import { UrlsTitle } from "@/enums/urls";
import { FunctionComponent } from "preact";
import { useLocation } from "preact-iso";
import classes from "./404.module.scss";
const Page404: FunctionComponent = () => {
const { route } = useLocation();
return (
<div id={classes.container}>
<div id={classes.main_container}>
<p class="text-6xl font-semibold">404</p>
<Button
onClick={() => {
route("/", true);
}}
color="secondary"
>
На главную
</Button>
</div>
</div>
);
};
export default withTitle(UrlsTitle.PAGE404, Page404);

4
src/pages/login.dto.ts Normal file
View File

@@ -0,0 +1,4 @@
export interface ILoginForm {
login: string;
password: string;
}

View File

@@ -5,5 +5,9 @@
} }
.login_card { .login_card {
@apply flex w-[95%] min-w-[300px] flex-col justify-center gap-2 rounded-md border-gray-400 p-5 shadow-md md:w-[350px]; @apply flex h-fit w-[95%] min-w-[300px] flex-col justify-around rounded-[3rem] bg-[linear-gradient(180.00deg,_rgba(239,251,194,0.53),rgb(206,232,251)_100%)] p-7 shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] md:w-[350px];
}
.login_card_name {
@apply mb-8 text-center text-2xl font-semibold;
} }

View File

@@ -1,28 +1,81 @@
import Button from "@/components/ui/Button"; import Button from "@/components/ui/Button";
import Input from "@/components/ui/Input"; import Input from "@/components/ui/Input";
import { withTitle } from "@/constructors/Component";
import { UrlsTitle } from "@/enums/urls";
import { useAppContext } from "@/providers/AuthProvider"; import { useAppContext } from "@/providers/AuthProvider";
import { FunctionComponent } from "preact"; import { FunctionComponent } from "preact";
import { useLocation } from "preact-iso"; import { useLocation } from "preact-iso";
import "preact/debug";
import { Controller, SubmitHandler, useForm } from "react-hook-form";
import { ILoginForm } from "./login.dto";
import classes from "./login.module.scss"; import classes from "./login.module.scss";
const testUser = {
login: "test",
password: "test",
};
const LoginPage: FunctionComponent = () => { const LoginPage: FunctionComponent = () => {
const { isLoggedIn } = useAppContext(); const { isLoggedIn } = useAppContext();
const location = useLocation(); const { route } = useLocation();
return ( const { control, handleSubmit, formState, setError } = useForm({
defaultValues: {
login: "",
password: "",
},
mode: "onChange",
});
const login: SubmitHandler<ILoginForm> = async (data) => {
console.log(data);
if (data.login !== testUser.login || data.password !== testUser.password) {
setError("login", { message: "Неверный логин или пароль" });
setError("password", { message: "Неверный логин или пароль" });
return;
}
isLoggedIn.value = true;
localStorage.setItem("loggedIn", "true");
route("/profile/tasks", true);
};
if (isLoggedIn.value) route("/profile/tasks", true);
return !isLoggedIn.value ? (
<div class={classes.login_container}> <div class={classes.login_container}>
<div class={classes.login_card}> <div class={classes.login_card}>
<Input placeholder="Login" textAlign="center" /> <p class={classes.login_card_name}>Антихвост</p>
<Input isPassword placeholder="Password" textAlign="center" /> <form onSubmit={handleSubmit(login)}>
<Button <Controller
onClick={() => { name="login"
isLoggedIn.value = true; control={control}
location.route("/profile/settings"); rules={{
required: "Введите логин",
}} }}
> render={({ field }) => (
Login <Input placeholder="Логин" textAlign="center" error={formState.errors.login?.message} {...field} />
)}
/>
<Controller
name="password"
control={control}
rules={{
required: "Введите пароль",
}}
render={({ field }) => (
<Input
placeholder="Пароль"
textAlign="center"
type="password"
error={formState.errors.password?.message}
{...field}
/>
)}
/>
<Button type="submit" color="secondary" className="w-full">
Войти
</Button> </Button>
</form>
</div> </div>
</div> </div>
) : (
<p>Redirecting...</p>
); );
}; };
export default LoginPage; export default withTitle(UrlsTitle.LOGIN, LoginPage);

View File

@@ -1 +1,13 @@
@reference "../index.scss"; @reference "../index.scss";
#main_container {
@apply flex h-screen flex-col md:flex-row;
}
#router_container {
@apply flex-1 overflow-y-auto px-3 pb-[6rem] break-all md:pb-5;
}
#menu_container {
@apply md:sticky md:top-0 md:h-screen;
}

View File

@@ -1,88 +1,20 @@
import Menu from "@/components/menu"; import Menu from "@/components/menu";
import { useAppContext } from "@/providers/AuthProvider";
import { FunctionComponent } from "preact"; import { FunctionComponent } from "preact";
import { Route, Router, useLocation } from "preact-iso"; import { lazy, Route, Router, useLocation } from "preact-iso";
import ids from "./profile.module.scss";
const ProfilePage: FunctionComponent = () => { const ProfilePage: FunctionComponent = () => {
const { route } = useLocation(); const { route } = useLocation();
return ( const { isLoggedIn } = useAppContext();
<div class="flex min-w-screen flex-row justify-between"> if (!isLoggedIn.value) route("/login", true);
<div class="w-full overflow-x-hidden overflow-y-auto px-3 pb-20 break-all md:pb-0"> return isLoggedIn.value ? (
<div id={ids.main_container}>
<div id={ids.router_container}>
<Router> <Router>
<Route <Route path="/settings" component={lazy(() => import("./profile_settings"))} />
path="/settings" <Route path="/tasks" component={lazy(() => import("./profile_tasks"))} />
component={() => ( <Route path="/calendar" component={lazy(() => import("./profile_calendar"))} />
<p class="text-2xl">
information sets circus station three entirely outer fun event free thirty breathing race snow planned
surface fully he cast town group plant egg journeyearly city thread fight outline wear key flame special
voice carefully exactly entire notice practical biggest television find particular middle noise cloud
tobacco cameraowner rule factor lift within order somewhere rocky powerful steam expect forty tonight
son front motor knowledge law easily equipment worse adult straw declaredhave famous choose ants us
broke warn prevent follow depth been buffalo bag classroom before term repeat want here plant division
camera hang preparechurch call member it build rose who potatoes easy sides exclaimed foot independent
point alone gone forty completely habit slept heard stand forest paperjoy repeat shot wrote label giving
whale quick tomorrow feathers additional active unless reader angle copy blanket white pitch my seeing
instant result asideexcited front word guess each wolf leader steady rhyme sound thy chapter during
electricity available managed comfortable eight left scientist article which stove term information sets
circus station three entirely outer fun event free thirty breathing race snow planned surface fully he
cast town group plant egg journeyearly city thread fight outline wear key flame special voice carefully
exactly entire notice practical biggest television find particular middle noise cloud tobacco
cameraowner rule factor lift within order somewhere rocky powerful steam expect forty tonight son front
motor knowledge law easily equipment worse adult straw declaredhave famous choose ants us broke warn
prevent follow depth been buffalo bag classroom before term repeat want here plant division camera hang
preparechurch call member it build rose who potatoes easy sides exclaimed foot independent point alone
gone forty completely habit slept heard stand forest paperjoy repeat shot wrote label giving whale quick
tomorrow feathers additional active unless reader angle copy blanket white pitch my seeing instant
result asideexcited front word guess each wolf leader steady rhyme sound thy chapter during electricity
available managed comfortable eight left scientist article which stove terminformation sets circus
station three entirely outer fun event free thirty breathing race snow planned surface fully he cast
town group plant egg journeyearly city thread fight outline wear key flame special voice carefully
exactly entire notice practical biggest television find particular middle noise cloud tobacco
cameraowner rule factor lift within order somewhere rocky powerful steam expect forty tonight son front
motor knowledge law easily equipment worse adult straw declaredhave famous choose ants us broke warn
prevent follow depth been buffalo bag classroom before term repeat want here plant division camera hang
preparechurch call member it build rose who potatoes easy sides exclaimed foot independent point alone
gone forty completely habit slept heard stand forest paperjoy repeat shot wrote label giving whale quick
tomorrow feathers additional active unless reader angle copy blanket white pitch my seeing instant
result asideexcited front word guess each wolf leader steady rhyme sound thy chapter during electricity
available managed comfortable eight left scientist article which stove terminformation sets circus
station three entirely outer fun event free thirty breathing race snow planned surface fully he cast
town group plant egg journeyearly city thread fight outline wear key flame special voice carefully
exactly entire notice practical biggest television find particular middle noise cloud tobacco
cameraowner rule factor lift within order somewhere rocky powerful steam expect forty tonight son front
motor knowledge law easily equipment worse adult straw declaredhave famous choose ants us broke warn
prevent follow depth been buffalo bag classroom before term repeat want here plant division camera hang
preparechurch call member it build rose who potatoes easy sides exclaimed foot independent point alone
gone forty completely habit slept heard stand forest paperjoy repeat shot wrote label giving whale quick
tomorrow feathers additional active unless reader angle copy blanket white pitch my seeing instant
result asideexcited front word guess each wolf leader steady rhyme sound thy chapter during electricity
available managed comfortable eight left scientist article which stove terminformation sets circus
station three entirely outer fun event free thirty breathing race snow planned surface fully he cast
town group plant egg journeyearly city thread fight outline wear key flame special voice carefully
exactly entire notice practical biggest television find particular middle noise cloud tobacco
cameraowner rule factor lift within order somewhere rocky powerful steam expect forty tonight son front
motor knowledge law easily equipment worse adult straw declaredhave famous choose ants us broke warn
prevent follow depth been buffalo bag classroom before term repeat want here plant division camera hang
preparechurch call member it build rose who potatoes easy sides exclaimed foot independent point alone
gone forty completely habit slept heard stand forest paperjoy repeat shot wrote label giving whale quick
tomorrow feathers additional active unless reader angle copy blanket white pitch my seeing instant
result asideexcited front word guess each wolf leader steady rhyme sound thy chapter during electricity
available managed comfortable eight left scientist article which stove terminformation sets circus
station three entirely outer fun event free thirty breathing race snow planned surface fully he cast
town group plant egg journeyearly city thread fight outline wear key flame special voice carefully
exactly entire notice practical biggest television find particular middle noise cloud tobacco
cameraowner rule factor lift within order somewhere rocky powerful steam expect forty tonight son front
motor knowledge law easily equipment worse adult straw declaredhave famous choose ants us broke warn
prevent follow depth been buffalo bag classroom before term repeat want here plant division camera hang
preparechurch call member it build rose who potatoes easy sides exclaimed foot independent point alone
gone forty completely habit slept heard stand forest paperjoy repeat shot wrote label giving whale quick
tomorrow feathers additional active unless reader angle copy blanket white pitch my seeing instant
result asideexcited front word guess each wolf leader steady rhyme sound thy chapter during electricity
available managed comfortable eight left scientist article which stove term
</p>
)}
/>
<Route path="/tasks" component={() => <p class="text-2xl">Tasks</p>} />
<Route path="/calendar" component={() => <p class="text-2xl">Calendar</p>} />
<Route <Route
default default
component={() => { component={() => {
@@ -92,8 +24,12 @@ const ProfilePage: FunctionComponent = () => {
/> />
</Router> </Router>
</div> </div>
<div id={ids.menu_container}>
<Menu /> <Menu />
</div> </div>
</div>
) : (
<p>Redirecting...</p>
); );
}; };

View File

@@ -0,0 +1,424 @@
import ModalCalendar from "@/components/ModalCalendar";
import ModalTags, { ITags } from "@/components/ModalTags";
import Task from "@/components/task";
import Dialog from "@/components/ui/Dialog";
import ModalWindow from "@/components/ui/Modal";
import { withTitle } from "@/constructors/Component";
import { UrlsTitle } from "@/enums/urls";
import { cn } from "@/utils/class-merge";
import {
BookOpenIcon,
CalendarDaysIcon,
DocumentDuplicateIcon,
InboxArrowDownIcon,
PencilIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { FunctionComponent } from "preact";
import { useEffect, useState } from "preact/hooks";
import { Calendar, CalendarDateTemplateEvent } from "primereact/calendar";
import { Nullable } from "primereact/ts-helpers";
import { useForm } from "react-hook-form";
import { ITask, ITaskForm } from "./profile_tasks.dto";
const example_tags: { first: string[]; second: string[] } = {
first: ["Программирование", "Информатика", "Физика", "Математика"],
second: ["Лабораторная работа", "Практическая работа", "Домашнее задание", "Экзамен"],
};
const calendarStyles = {
root: "inline-flex w-full relative",
input: "font-sans text-base text-gray-600 bg-white p-3",
panel: "bg-white min-w-full p-2",
header: "flex items-center justify-between p-2 text-gray-700 font-semibold m-0 mb-4",
title: "leading-8 mx-auto text-xl",
previousButton: "flex items-center justify-center cursor-pointer w-8 h-8 text-gray-600",
nextButton: "flex items-center justify-center cursor-pointer w-8 h-8 text-gray-600",
table: "border-collapse w-full",
tableHeaderCell: "p-2 text-center",
weekDay: "text-gray-600 font-normal pb-4",
monthTitle: "text-gray-700 font-semibold mr-2",
yearTitle: "text-gray-700 font-semibold",
monthPicker: "grid grid-cols-3 gap-2",
yearPicker: "grid grid-cols-2 gap-2",
month:
"p-2 text-center cursor-pointer rounded-lg hover:bg-[rgba(251,194,199,0.1)] [&.p-highlight]:bg-[rgba(251,194,199,0.2)]",
year: "p-2 text-center cursor-pointer rounded-lg hover:bg-[rgba(251,194,199,0.1)] [&.p-highlight]:bg-[rgba(251,194,199,0.2)]",
};
const ProfileCalendar: FunctionComponent = () => {
const [currentDate, setCurrentDate] = useState(new Date());
const [tasks, setTasks] = useState<ITask[]>([]);
const [selectedTasks, setSelectedTasks] = useState<ITask[]>([]);
const [openModal, setIsOpen] = useState(false);
const [openModalCalendar, setOpenModalCalendar] = useState(false);
const [openModalTags, setOpenModalTags] = useState(false);
const [isEdit, setIsEdit] = useState(false);
const [isEditModal, setIsEditModal] = useState(false);
const [editContent, setEditContent] = useState<ITask | null>(null);
const [calendarDate, setCalendarDate] = useState<Nullable<Date>>();
const [tags, setTags] = useState<ITags>({ first: "", second: "", overdue: false });
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const {
handleSubmit,
register,
reset,
setError,
formState: { errors },
} = useForm<ITaskForm>({
defaultValues: {
tags: [],
},
});
useEffect(() => {
const storedTasks = localStorage.getItem("tasks");
if (storedTasks) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const parsedTasks: ITask[] = JSON.parse(storedTasks).map((task: any) => ({
...task,
date: new Date(task.date),
}));
setTasks(parsedTasks);
}
}, []);
const handleDeleteTask = () => {
if (!editContent) return;
setTasks(tasks.filter((task) => task.id !== editContent.id));
setIsOpen(false);
setShowDeleteDialog(false);
};
useEffect(() => {
if (!currentDate) return;
const tasksForDate = tasks.filter((task) => {
const taskDate = task.date;
return (
taskDate.getDate() === currentDate.getDate() &&
taskDate.getMonth() === currentDate.getMonth() &&
taskDate.getFullYear() === currentDate.getFullYear()
);
});
setSelectedTasks(tasksForDate);
}, [currentDate, tasks]);
useEffect(() => {
if (editContent) reset({ ...editContent, date: editContent.date.toISOString().slice(0, 16) });
else reset();
}, [editContent]);
useEffect(() => {
localStorage.setItem("tasks", JSON.stringify(tasks));
}, [tasks]);
useEffect(() => {
if (!editContent) return;
const newEditContent = editContent;
if (tags.first) newEditContent.tags = [tags.first, newEditContent.tags[1]];
if (tags.second) newEditContent.tags = [newEditContent.tags[0], tags.second];
setEditContent(newEditContent);
}, [tags]);
const tasksCount = (date: CalendarDateTemplateEvent) => {
return tasks.filter((task) => {
const taskDate = task.date;
return (
taskDate.getDate() === date.day && taskDate.getMonth() === date.month && taskDate.getFullYear() === date.year
);
}).length;
};
const hasTasksOnDate = (date: CalendarDateTemplateEvent) => {
return tasks.some((task) => {
const taskDate = task.date;
return (
taskDate.getDate() === date.day && taskDate.getMonth() === date.month && taskDate.getFullYear() === date.year
);
});
};
const dateTemplate = (date: CalendarDateTemplateEvent) => {
const isHighlighted = hasTasksOnDate(date);
const countT = tasksCount(date);
const isSelected =
currentDate &&
currentDate.getDate() === date.day &&
currentDate.getMonth() === date.month &&
currentDate.getFullYear() === date.year;
const isToday =
new Date().getDate() === date.day &&
new Date().getMonth() === date.month &&
new Date().getFullYear() === date.year;
return (
<div
className={cn(
"relative flex h-20 w-full cursor-pointer items-start justify-start rounded-lg p-2",
"hover:bg-[rgba(251,194,199,0.1)]",
{
"bg-[rgba(251,194,199,0.4)]": isSelected,
"bg-[rgba(251,194,199,0.2)]": isToday && !isSelected,
}
)}
>
<span>{date.day}</span>
{isHighlighted && (
<div class="absolute top-2 right-2 flex h-fit w-2 flex-col items-center gap-1 md:h-2 md:w-fit md:flex-row">
{Array.from({ length: countT > 3 ? 3 : countT }).map((_, i) => (
<span key={i} className="size-2 rounded-full bg-pink-400" />
))}
{countT > 3 && <span className="text-xs font-bold text-pink-400 select-none">+</span>}
</div>
)}
</div>
);
};
const handleTaskCheck = (taskId: string) => {
const updatedTasks = tasks.map((task) => (task.id === taskId ? { ...task, checked: !task.checked } : task));
setTasks(updatedTasks);
localStorage.setItem("tasks", JSON.stringify(updatedTasks));
};
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat("ru-RU", {
day: "numeric",
month: "long",
year: "numeric",
}).format(date);
};
const saveTask = (data: ITaskForm) => {
if (!calendarDate) {
setError("date", { message: "Выберите дату" });
return;
}
if ((!editContent?.tags[0] || !editContent.tags[1]) && (!tags.first || !tags.second)) {
setError("tags", { message: "Выберите теги" });
return;
}
const eTask: ITask = {
...data,
date: calendarDate,
tags: editContent?.tags.length ? editContent.tags : [tags.first, tags.second],
new: true,
};
setTasks(tasks.map((task) => (task.id === eTask.id ? eTask : task)));
localStorage.setItem("tasks", JSON.stringify(tasks.map((task) => (task.id === eTask.id ? eTask : task))));
setTags({ first: "", second: "", overdue: false });
};
const pt = {
root: { className: calendarStyles.root },
input: { root: { className: calendarStyles.input } },
panel: { className: calendarStyles.panel },
header: { className: calendarStyles.header },
title: { className: calendarStyles.title },
previousButton: { className: calendarStyles.previousButton },
nextButton: { className: calendarStyles.nextButton },
table: { className: calendarStyles.table },
tableHeaderCell: { className: calendarStyles.tableHeaderCell },
weekDay: { className: calendarStyles.weekDay },
monthTitle: { className: calendarStyles.monthTitle },
yearTitle: { className: calendarStyles.yearTitle },
monthPicker: { className: calendarStyles.monthPicker },
yearPicker: { className: calendarStyles.yearPicker },
month: { className: calendarStyles.month },
year: { className: calendarStyles.year },
};
return (
<div class="flex w-full flex-col items-center">
<ModalTags
isOpen={openModalTags}
setIsOpen={setOpenModalTags}
tagsList={example_tags}
value={tags}
onClose={() => {
setTags({ first: "", second: "", overdue: false });
}}
onChange={setTags}
/>
<ModalCalendar
isOpen={openModalCalendar}
setIsOpen={setOpenModalCalendar}
onClose={() => {
if (isEdit && !isEditModal) setCalendarDate(null);
}}
onChange={(e) => isEditModal && setCalendarDate(e.value)}
value={calendarDate!}
/>
<ModalWindow
isOpen={openModal}
setIsOpen={setIsOpen}
onClose={() => {
setIsEdit(false);
setEditContent(null);
setIsEditModal(false);
setTags({ first: "", second: "", overdue: false });
setCalendarDate(null);
}}
>
{isEdit && editContent && (
<form
class="flex h-full w-full flex-col items-start justify-between"
onSubmit={(e) => {
e.preventDefault();
if (isEditModal) handleSubmit(saveTask)();
else setIsEditModal(!isEditModal);
}}
>
<div class="flex w-full flex-row items-start justify-between">
<div class="flex flex-1 flex-col gap-1 pe-2">
<input
class="w-full text-2xl outline-0"
disabled={!isEditModal}
placeholder="Название"
{...register("name", {
required: "Заполните название",
maxLength: { value: 20, message: "Максимум 20 символов в названии" },
})}
/>
<textarea
class="h-[5rem] w-full resize-none outline-0"
disabled={!isEditModal}
placeholder={isEditModal ? "Описание" : ""}
{...register("description", {
maxLength: { value: 200, message: "Максимум 200 символов в описании" },
})}
/>
<input
type="datetime-local"
value={calendarDate ? calendarDate.toISOString().slice(0, 16) : ""}
hidden
{...register("date")}
/>
<input type="checkbox" hidden {...register("checked")} />
</div>
<div class="flex flex-row gap-4">
<div
className="flex cursor-pointer flex-col items-center gap-3"
onClick={() => {
if (isEditModal) {
handleSubmit(saveTask)();
setIsEditModal(!isEditModal);
} else setIsEditModal(!isEditModal);
}}
>
{isEditModal ? (
<>
<InboxArrowDownIcon class="size-6" />
<p class="text-[0.7rem]">Сохранить</p>
</>
) : (
<>
<PencilIcon class="size-6" />
<p class="text-[0.7rem]">Редактировать</p>
</>
)}
</div>
<div
className="flex cursor-pointer flex-col items-center gap-3"
onClick={() => setShowDeleteDialog(true)}
>
<TrashIcon class="size-6" />
<p class="text-[0.7rem]">Удалить</p>
</div>
</div>
</div>
{errors.name && <p class="text-red-500">{errors.name.message}</p>}
{errors.description && <p class="text-red-500">{errors.description.message}</p>}
{errors.date && <p class="text-red-500">{errors.date.message}</p>}
{errors.tags && <p class="text-red-500">{errors.tags.message}</p>}
<div class="flex flex-col items-center gap-6 self-center md:flex-row md:justify-start md:self-start">
<div
class={cn("flex h-full flex-row items-center gap-1 rounded-2xl bg-[rgba(251,194,199,0.38)] px-2 py-1", {
"cursor-pointer": isEditModal,
})}
onClick={() => {
if (!isEditModal) return;
setOpenModalCalendar(true);
setCalendarDate(editContent.date);
}}
>
<CalendarDaysIcon class="size-10" />
<p>
{Intl.DateTimeFormat("ru-RU", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(calendarDate!)}
</p>
</div>
<div
class={cn("flex h-full flex-col items-start gap-1 rounded-2xl bg-[rgba(251,194,199,0.38)] px-4 py-2", {
"cursor-pointer": isEditModal,
})}
onClick={() => {
if (!isEditModal) return;
setTags({ first: editContent.tags[0], second: editContent.tags[1], overdue: false });
setOpenModalTags(true);
}}
>
<p class="flex flex-row gap-2">
<BookOpenIcon class="size-5" />
{editContent.tags[0]}
</p>
<p class="flex flex-row gap-2">
<DocumentDuplicateIcon class="size-5" />
{editContent.tags[1]}
</p>
</div>
</div>
</form>
)}
</ModalWindow>
<Dialog
isOpen={showDeleteDialog}
setIsOpen={setShowDeleteDialog}
title="Удаление задачи"
content="Вы уверены, что хотите удалить эту задачу?"
onConfirm={handleDeleteTask}
confirmText="Удалить"
cancelText="Отмена"
/>
<Calendar
value={currentDate}
onChange={(e) => setCurrentDate(e ? e.value! : new Date())}
inline
pt={pt}
dateTemplate={dateTemplate}
className="[&_.p-datepicker]:!border-0 [&_.p-datepicker-calendar]:border-separate [&_.p-datepicker-calendar]:border-spacing-2 [&_td]:rounded-lg [&_td]:border [&_td]:border-[rgba(251,194,199,0.38)]"
/>
<div class="mt-8 w-full px-4">
<h2 class="mb-4 text-2xl font-semibold">Задачи на {formatDate(currentDate)}</h2>
{selectedTasks.length > 0 ? (
<div class="flex flex-col gap-4">
{selectedTasks.map((task) => (
<Task
key={task.id}
name={task.name}
checked={task.checked}
onClick={() => {
setIsOpen(true);
setIsEdit(true);
setEditContent(task);
setCalendarDate(task.date);
}}
onMarkClick={() => handleTaskCheck(task.id)}
/>
))}
</div>
) : (
<div class="rounded-lg bg-[rgba(251,194,199,0.2)] p-4 text-center">На этот день задач нет</div>
)}
</div>
</div>
);
};
export default withTitle(UrlsTitle.CALENDAR, ProfileCalendar);

View File

@@ -0,0 +1,24 @@
@reference "../index.scss";
.container {
@apply flex h-full w-full flex-col items-center px-6 pt-8 md:items-start;
}
#avatar {
@apply flex aspect-square h-[7rem] flex-col items-center justify-center rounded-full bg-white;
}
.profile_container {
@apply flex h-full w-full flex-col items-center;
}
.settings_block__buttons {
@apply flex w-[300px] flex-col gap-10;
}
.header_block__name {
@apply flex h-fit w-full flex-1 flex-col items-center justify-start gap-3 px-10;
}
.settings_block {
@apply mt-12 flex h-full w-full flex-col items-center justify-start md:mt-0 md:justify-center;
}

View File

@@ -0,0 +1,82 @@
import Button from "@/components/ui/Button";
import { withTitle } from "@/constructors/Component";
import { UrlsTitle } from "@/enums/urls";
import { useAppContext } from "@/providers/AuthProvider";
import { cn } from "@/utils/class-merge";
import { calculatePoints, getCurrentStatus } from "@/utils/status-system";
import { ArrowRightStartOnRectangleIcon, Cog8ToothIcon } from "@heroicons/react/24/outline";
import { FunctionComponent } from "preact";
import { useLocation } from "preact-iso";
import { useEffect, useState } from "preact/hooks";
import classes from "./profile_settings.module.scss";
const ProfileSettings: FunctionComponent = () => {
const { isLoggedIn } = useAppContext();
const { route } = useLocation();
const [status, setStatus] = useState(0);
const maxStatus = 100;
useEffect(() => {
const updateStatus = () => {
const tasks = JSON.parse(localStorage.getItem("tasks") || "[]");
const completedTasks = tasks.filter((task: { checked: boolean }) => task.checked).length;
const points = calculatePoints(completedTasks);
setStatus(points);
};
// Initial update
updateStatus();
// Update when tasks change
const handleStorage = (e: StorageEvent) => {
if (e.key === "tasks") {
updateStatus();
}
};
window.addEventListener("storage", handleStorage);
return () => window.removeEventListener("storage", handleStorage);
}, []);
return (
<div class={classes.container}>
<div class="flex w-full flex-col items-center rounded-[4rem] bg-[linear-gradient(180.00deg,rgb(251,194,199),rgba(206,232,251,0.72)_100%)] px-7 py-5 shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] md:flex-row">
<div id={classes.avatar}>Аватар</div>
<div class={classes.header_block__name}>
<p class="text-4xl font-semibold">Никнейм</p>
<p class="text-2xl font-light">{getCurrentStatus(status)}</p>
<div class="h-1.5 w-full overflow-hidden rounded-2xl bg-white">
<div
class={cn("relative top-0 left-0 h-2 bg-black")}
style={{ width: `${(status / maxStatus) * 100}%` }}
></div>
</div>
<div class="-mt-3 self-end text-sm font-light">
{status}/{maxStatus}
</div>
</div>
</div>
<div class={classes.profile_container}>
<div class={classes.settings_block}>
<div class={classes.settings_block__buttons}>
<Button className="flex flex-row items-center justify-center gap-2">
<Cog8ToothIcon class="size-8" /> Настройки
</Button>
<Button
className="flex flex-row items-center justify-center gap-2 bg-[linear-gradient(180.00deg,rgba(246,255,211,0.7),rgba(195,229,253,0.7)_100%)]"
onClick={() => {
isLoggedIn.value = false;
localStorage.setItem("loggedIn", "false");
route("/login", true);
}}
>
<ArrowRightStartOnRectangleIcon class="size-8" /> Выйти
</Button>
</div>
</div>
</div>
</div>
);
};
export default withTitle(UrlsTitle.PROFILE, ProfileSettings);

View File

@@ -0,0 +1,13 @@
export interface ITask {
id: string;
name: string;
checked: boolean;
date: Date;
description: string;
tags: string[];
new?: boolean;
}
export interface ITaskForm extends Omit<ITask, "date"> {
date: string;
}

View File

@@ -0,0 +1,13 @@
@reference "../index.scss";
.container {
@apply flex h-fit min-h-full w-full flex-col items-center gap-4 pt-3 md:px-6;
}
.header {
@apply mb-3 flex w-full flex-row items-center justify-between text-xl font-semibold sm:text-3xl md:mb-12 md:text-5xl;
}
.tasks_container {
@apply flex w-full flex-col items-center gap-3 md:items-start md:gap-10;
}

800
src/pages/profile_tasks.tsx Normal file
View File

@@ -0,0 +1,800 @@
//! Файл большой, потому что было лень делить на компоненты
import Task from "@/components/task";
import ModalCalendar from "@/components/ModalCalendar";
import ModalTags, { ITags } from "@/components/ModalTags";
import Button from "@/components/ui/Button";
import Dialog from "@/components/ui/Dialog";
import ModalWindow from "@/components/ui/Modal";
import { withTitle } from "@/constructors/Component";
import { UrlsTitle } from "@/enums/urls";
import { cn } from "@/utils/class-merge";
import { PlusIcon } from "@heroicons/react/20/solid";
import {
BookmarkIcon,
BookOpenIcon,
CalendarDaysIcon,
DocumentDuplicateIcon,
FunnelIcon,
InboxArrowDownIcon,
MagnifyingGlassIcon,
PencilIcon,
TrashIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import { FunctionComponent } from "preact";
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { Checkbox, CheckboxPassThroughMethodOptions } from "primereact/checkbox";
import { Nullable } from "primereact/ts-helpers";
import { SubmitHandler, useForm } from "react-hook-form";
import { v4 as uuid } from "uuid";
import { ITask, ITaskForm } from "./profile_tasks.dto";
import classes from "./profile_tasks.module.scss";
const example_tags: { first: string[]; second: string[] } = {
first: ["Программирование", "Информатика", "Физика", "Математика"],
second: ["Лабораторная работа", "Практическая работа", "Домашнее задание", "Экзамен"],
};
const ProfileTasks: FunctionComponent = () => {
const [openModal, setIsOpen] = useState(false); // Открыта модалка
const [openModalCalendar, setOpenModalCalendar] = useState(false); // Открыта модалка календаря
const [openModalTags, setOpenModalTags] = useState(false); // Открыта модалка тегов
const [openSearchModal, setOpenSearchModal] = useState(false); // Открыто окно поиска
const [openFilterModal, setOpenFilterModal] = useState(false);
const [isEdit, setIsEdit] = useState(false); // Открыта задача
const [isEditModal, setIsEditModal] = useState(false); // Включено редактирование задачи
const [isCreating, setIsCreating] = useState(false); // Включено создание задачи
const [editContent, setEditContent] = useState<ITask | null>(null); // Содержимое редактируемой задачи
const [calendarDate, setCalendarDate] = useState<Nullable<Date>>(); // Выбранная в календаре дата
const [tags, setTags] = useState<ITags>({ first: "", second: "", overdue: false });
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [searchQuery, setSearchQuery] = useState(""); // Текст поиска
const [filterTags, setFilterTags] = useState<ITags>({ first: "", second: "", overdue: false });
const [openFirstList, setOpenFirstList] = useState(false);
const [openSecondList, setOpenSecondList] = useState(false);
const getDate = useMemo(() => {
const date = new Date();
const formatter = new Intl.DateTimeFormat("ru-RU", { month: "long", day: "numeric" });
return formatter.format(date);
}, []);
const init_tasks: ITask[] = localStorage.getItem("tasks") ? JSON.parse(localStorage.getItem("tasks") as string) : [];
let clear = false;
init_tasks.forEach((task) => {
clear = clear || (task.new == undefined ? true : false);
if (!clear) task.new = true;
task.date = new Date(task.date);
});
const [tasks, setTasks] = useState<ITask[]>(clear ? [] : init_tasks);
useEffect(() => {
localStorage.setItem("tasks", JSON.stringify(tasks));
}, [tasks]);
const {
handleSubmit,
register,
reset,
setError,
formState: { errors },
} = useForm<ITaskForm>({
defaultValues: {
tags: [],
},
});
const saveTask: SubmitHandler<ITaskForm> = (data) => {
if (!calendarDate) {
setError("date", { message: "Выберите дату" });
return;
}
console.log(tags);
if ((!editContent?.tags[0] || !editContent.tags[1]) && (!tags.first || !tags.second)) {
setError("tags", { message: "Выберите теги" });
return;
}
const eTask: ITask = {
...data,
date: calendarDate,
tags: editContent?.tags.length ? editContent.tags : [tags.first, tags.second],
new: true,
};
if (isCreating) setTasks([...tasks, eTask]);
else setTasks(tasks.map((task) => (task.id === eTask.id ? eTask : task)));
if (isCreating) setIsOpen(false);
setTags({ first: "", second: "", overdue: false });
};
useEffect(() => {
if (editContent) reset({ ...editContent, date: editContent.date.toISOString().slice(0, 16) });
else reset();
}, [editContent]);
useEffect(() => {
reset({
name: "",
description: "",
date: "",
tags: [],
checked: false,
});
}, [isCreating]);
useEffect(() => {
if (!editContent) return;
const newEditContent = editContent;
if (tags.first) newEditContent.tags = [tags.first, newEditContent.tags[1]];
if (tags.second) newEditContent.tags = [newEditContent.tags[0], tags.second];
setEditContent(newEditContent);
}, [tags]);
const groupTasksByDate = useMemo(() => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
const groupedTasks = {
today: [] as ITask[],
tomorrow: [] as ITask[],
future: [] as { date: Date; tasks: ITask[] }[],
};
tasks.forEach((task) => {
const taskDate = new Date(task.date);
taskDate.setHours(0, 0, 0, 0);
if (taskDate.getTime() === today.getTime()) {
groupedTasks.today.push(task);
} else if (taskDate.getTime() === tomorrow.getTime()) {
groupedTasks.tomorrow.push(task);
} else if (taskDate > tomorrow) {
const existingGroup = groupedTasks.future.find((group) => group.date.getTime() === taskDate.getTime());
if (existingGroup) {
existingGroup.tasks.push(task);
} else {
groupedTasks.future.push({ date: taskDate, tasks: [task] });
}
}
});
// Sort tasks within each group
groupedTasks.today.sort((a, b) => a.date.getTime() - b.date.getTime());
groupedTasks.tomorrow.sort((a, b) => a.date.getTime() - b.date.getTime());
groupedTasks.future.sort((a, b) => a.date.getTime() - b.date.getTime());
groupedTasks.future.forEach((group) => {
group.tasks.sort((a, b) => a.date.getTime() - b.date.getTime());
});
return groupedTasks;
}, [tasks]);
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat("ru-RU", {
day: "numeric",
month: "long",
year: "numeric",
}).format(date);
};
const handleDeleteTask = () => {
if (!editContent) return;
setTasks(tasks.filter((task) => task.id !== editContent.id));
setIsOpen(false);
setShowDeleteDialog(false);
};
const filteredTasks = useMemo(() => {
let filtered = tasks;
// Фильтрация по поиску
if (searchQuery) {
filtered = filtered.filter(
(task) =>
task.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
task.description?.toLowerCase().includes(searchQuery.toLowerCase())
);
}
// Фильтрация по тегам
if (filterTags.first || filterTags.second) {
filtered = filtered.filter(
(task) =>
(!filterTags.first || task.tags[0] === filterTags.first) &&
(!filterTags.second || task.tags[1] === filterTags.second)
);
}
filtered = filtered.filter((task) => (filterTags.overdue ? task.date < new Date() : task.date >= new Date()));
return filtered;
}, [tasks, searchQuery, filterTags]);
const searchInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (searchInputRef.current && openSearchModal) searchInputRef.current.focus();
}, [searchInputRef, openSearchModal]);
return (
<div class={classes.container}>
<ModalTags
zIndex={70}
isOpen={openModalTags}
setIsOpen={setOpenModalTags}
tagsList={example_tags}
value={tags}
onClose={() => {
if (!isCreating) setTags({ first: "", second: "", overdue: false });
}}
onChange={setTags}
/>
<ModalCalendar
zIndex={80}
isOpen={openModalCalendar}
setIsOpen={setOpenModalCalendar}
onClose={() => {
if (isEdit && !isEditModal) setCalendarDate(null);
}}
onChange={(e) => (isCreating || isEditModal) && setCalendarDate(e.value)}
value={calendarDate!}
/>
<ModalWindow
zIndex={60}
isOpen={openModal}
setIsOpen={setIsOpen}
onClose={() => {
setIsEdit(false);
setEditContent(null);
setIsCreating(false);
setIsEditModal(false);
setTags({ first: "", second: "", overdue: false });
setCalendarDate(null);
}}
>
{isEdit && editContent && (
<form
class="flex h-full w-full flex-col items-start justify-between"
onSubmit={(e) => {
e.preventDefault();
if (isEditModal) handleSubmit(saveTask)();
else setIsEditModal(!isEditModal);
}}
>
<div class="flex w-full flex-row items-start justify-between">
<div class="flex flex-1 flex-col gap-1 pe-2">
<input
class={cn("w-full p-2 text-2xl outline-0", {
"rounded-md bg-gray-400/30 ring-1 ring-gray-400": isEditModal,
})}
disabled={!isEditModal}
placeholder="Название"
{...register("name", {
required: "Заполните название",
maxLength: { value: 20, message: "Максимум 20 символов в названии" },
})}
/>
<textarea
class={cn("h-[5rem] w-full resize-none p-2 outline-0", {
"rounded-md bg-gray-400/30 ring-1 ring-gray-400": isEditModal,
})}
disabled={!isEditModal}
placeholder={isEditModal ? "Описание" : ""}
{...register("description", {
maxLength: { value: 200, message: "Максимум 200 символов в описании" },
})}
/>
<input
type="datetime-local"
value={calendarDate ? calendarDate.toISOString().slice(0, 16) : ""}
hidden
{...register("date")}
/>
<input type="checkbox" hidden {...register("checked")} />
</div>
<div class="flex flex-col gap-4 md:flex-row">
<div
className="flex cursor-pointer flex-col items-center gap-3"
onClick={() => {
if (isEditModal) {
handleSubmit(saveTask)();
setIsEditModal(!isEditModal);
} else setIsEditModal(!isEditModal);
}}
>
{isEditModal ? (
<>
<InboxArrowDownIcon class="size-6" />
<p class="text-[0.7rem]">Сохранить</p>
</>
) : (
<>
<PencilIcon class="size-6" />
<p class="text-[0.7rem]">Редактировать</p>
</>
)}
</div>
<div
className="flex cursor-pointer flex-col items-center gap-3"
onClick={() => setShowDeleteDialog(true)}
>
<TrashIcon class="size-6" />
<p class="text-[0.7rem]">Удалить</p>
</div>
</div>
</div>
{errors.name && <p class="text-red-500">{errors.name.message}</p>}
{errors.description && <p class="text-red-500">{errors.description.message}</p>}
{errors.date && <p class="text-red-500">{errors.date.message}</p>}
{errors.tags && <p class="text-red-500">{errors.tags.message}</p>}
<div class="flex flex-col items-center gap-6 self-center md:flex-row md:justify-start md:self-start">
<div
class={cn("flex h-full flex-row items-center gap-1 rounded-2xl bg-[rgba(251,194,199,0.38)] px-2 py-1", {
"cursor-pointer": isEditModal,
})}
onClick={() => {
if (!isEditModal) return;
setOpenModalCalendar(true);
setCalendarDate(editContent.date);
}}
>
<CalendarDaysIcon class="size-10" />
<p>
{Intl.DateTimeFormat("ru-RU", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(calendarDate!)}
</p>
</div>
<div
class={cn("flex h-full flex-col items-start gap-1 rounded-2xl bg-[rgba(251,194,199,0.38)] px-4 py-2", {
"cursor-pointer": isEditModal,
})}
onClick={() => {
if (!isEditModal) return;
setTags({ first: editContent.tags[0], second: editContent.tags[1], overdue: false });
setOpenModalTags(true);
}}
>
<p class="flex flex-row gap-2">
<BookOpenIcon class="size-5" />
{editContent.tags[0]}
</p>
<p class="flex flex-row gap-2">
<DocumentDuplicateIcon class="size-5" />
{editContent.tags[1]}
</p>
</div>
</div>
</form>
)}
{isCreating && (
<form
class="flex h-full w-full flex-col items-start justify-between"
onSubmit={(e) => {
e.preventDefault();
handleSubmit((data) => saveTask({ ...data, id: uuid() }))();
}}
>
<div class="flex w-full flex-1 flex-row items-start justify-between">
<div class="flex flex-1 flex-col gap-1 pe-2">
<input
class="w-full rounded-md bg-gray-400/30 p-2 text-2xl ring-1 ring-gray-400 outline-0"
maxLength={20}
placeholder="Название"
{...register("name", { required: "Заполните название" })}
/>
<textarea
class="h-[5rem] w-full resize-none rounded-md bg-gray-400/30 p-2 ring-1 ring-gray-400 outline-0"
placeholder="Описание"
maxLength={200}
{...register("description")}
/>
<input
type="datetime-local"
value={calendarDate ? calendarDate.toISOString().slice(0, 16) : ""}
hidden
{...register("date")}
/>
<input type="checkbox" checked={false} hidden {...register("checked")} />
</div>
<div class="flex flex-col gap-3 self-start md:flex-row">
<CalendarDaysIcon
class="size-8 cursor-pointer"
onClick={() => {
setOpenModalCalendar(true);
setCalendarDate(calendarDate ?? new Date());
}}
/>
<BookmarkIcon class="size-8 cursor-pointer" onClick={() => setOpenModalTags(true)} />
</div>
</div>
{errors.name && <p class="text-red-500">{errors.name.message}</p>}
{errors.date && <p class="text-red-500">{errors.date.message}</p>}
{errors.tags && <p class="text-red-500">{errors.tags.message}</p>}
<div className="mb-8 flex h-16 flex-col items-center gap-6 self-center md:mb-0 md:flex-row md:self-start">
<Button
className="text-sm"
onClick={() => {
setIsOpen(false);
}}
>
Отмена
</Button>
<Button color="red" type="submit">
Добавить задачу
</Button>
</div>
</form>
)}
</ModalWindow>
<Dialog
isOpen={showDeleteDialog}
setIsOpen={setShowDeleteDialog}
title="Удаление задачи"
content="Вы уверены, что хотите удалить эту задачу?"
onConfirm={handleDeleteTask}
confirmText="Удалить"
cancelText="Отмена"
/>
{!searchQuery && !filterTags.first && !filterTags.second && !filterTags.overdue ? (
filteredTasks.length > 0 ? (
<>
<div class={classes.header}>
Сегодня: {getDate}
<div class="flex flex-row items-center gap-2 md:hidden">
{!openSearchModal && (
<div
class="flex aspect-square h-12 cursor-pointer flex-col items-center justify-center rounded-full bg-[rgba(206,232,251,0.7)] text-xl text-gray-600 shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] hover:bg-[rgba(206,232,251,0.9)]"
onClick={(e) => {
e.stopPropagation();
if (!openSearchModal) setOpenFilterModal(false);
setOpenSearchModal(!openSearchModal);
}}
>
<MagnifyingGlassIcon class="size-6" />
</div>
)}
<div class="flex aspect-square h-12 cursor-pointer flex-col items-center justify-center rounded-full bg-[rgba(206,232,251,0.7)] text-xl text-gray-600 shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] hover:bg-[rgba(206,232,251,0.9)]">
<FunnelIcon
class="size-6"
onClick={(e) => {
e.stopPropagation();
if (!openFilterModal) setOpenSearchModal(false);
setOpenFilterModal(!openFilterModal);
}}
/>
</div>
</div>
</div>
<div class={classes.tasks_container}>
{groupTasksByDate.today.length > 0 ? (
<div class="w-full">
{groupTasksByDate.today.map((task) => (
<Task
name={task.name}
key={task.id}
checked={task.checked}
overdue={task.date < new Date()}
onClick={() => {
setIsOpen(true);
setIsEdit(true);
setEditContent(task);
setCalendarDate(task.date);
}}
onMarkClick={() => {
setTasks(tasks.map((t) => (t.id === task.id ? { ...t, checked: !t.checked } : t)));
}}
/>
))}
</div>
) : (
<div class="w-full text-center text-xl">Задач на сегодня нет</div>
)}
{groupTasksByDate.tomorrow.length > 0 && (
<div class="flex w-full flex-col gap-3 md:gap-10">
<div class="w-full text-3xl font-semibold md:text-5xl">Завтра</div>
{groupTasksByDate.tomorrow.map((task) => (
<Task
name={task.name}
key={task.id}
checked={task.checked}
onClick={() => {
setIsOpen(true);
setIsEdit(true);
setEditContent(task);
setCalendarDate(task.date);
}}
onMarkClick={() => {
setTasks(tasks.map((t) => (t.id === task.id ? { ...t, checked: !t.checked } : t)));
}}
/>
))}
</div>
)}
{groupTasksByDate.future.map((group) => (
<div class="flex w-full flex-col gap-3 md:gap-10" key={group.date.getTime()}>
<div class="w-full text-3xl font-semibold md:text-5xl">{formatDate(group.date)}</div>
{group.tasks.map((task) => (
<Task
name={task.name}
key={task.id}
checked={task.checked}
onClick={() => {
setIsOpen(true);
setIsEdit(true);
setEditContent(task);
setCalendarDate(task.date);
}}
onMarkClick={() => {
setTasks(tasks.map((t) => (t.id === task.id ? { ...t, checked: !t.checked } : t)));
}}
/>
))}
</div>
))}
</div>
</>
) : (
<div class="flex w-full flex-1 flex-col items-center justify-center text-2xl">Начни уже сегодня!</div>
)
) : (
<div class={classes.tasks_container}>
<div class="mb-8 flex w-full flex-row items-center justify-between text-xl font-semibold sm:text-3xl md:text-5xl">
{searchQuery ? "Результаты поиска" : "Результаты фильтрации"}
<div class="flex flex-row items-center gap-2 md:hidden">
{!openSearchModal && (
<div
class="flex aspect-square h-12 cursor-pointer flex-col items-center justify-center rounded-full bg-[rgba(206,232,251,0.7)] text-xl text-gray-600 shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] hover:bg-[rgba(206,232,251,0.9)]"
onClick={(e) => {
e.stopPropagation();
if (!openSearchModal) setOpenFilterModal(false);
setOpenSearchModal(!openSearchModal);
}}
>
<MagnifyingGlassIcon class="size-6" />
</div>
)}
<div class="flex aspect-square h-12 cursor-pointer flex-col items-center justify-center rounded-full bg-[rgba(206,232,251,0.7)] text-xl text-gray-600 shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] hover:bg-[rgba(206,232,251,0.9)]">
<FunnelIcon
class="size-6"
onClick={(e) => {
e.stopPropagation();
if (!openFilterModal) setOpenSearchModal(false);
setOpenFilterModal(!openFilterModal);
}}
/>
</div>
</div>
</div>
{filteredTasks.length > 0 ? (
filteredTasks.map((task) => (
<Task
name={task.name}
key={task.id}
checked={task.checked}
overdue={task.date < new Date()}
onClick={() => {
setIsOpen(true);
setIsEdit(true);
setEditContent(task);
setCalendarDate(task.date);
}}
onMarkClick={() => {
setTasks(tasks.map((t) => (t.id === task.id ? { ...t, checked: !t.checked } : t)));
}}
/>
))
) : (
<div class="w-full text-center text-xl">
{searchQuery ? "Ничего не найдено" : "Нет задач с выбранными фильтрами"}
</div>
)}
</div>
)}
<div class="group fixed right-1 bottom-16 flex flex-row items-center justify-start space-x-3 overflow-x-hidden py-2 md:right-[22rem] md:bottom-4">
<div
class="flex aspect-square h-20 cursor-pointer items-center justify-center rounded-full bg-[rgb(251,194,199,0.53)] text-9xl text-white shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] transition-all duration-300 ease-out group-hover:ml-[12rem] hover:bg-[rgb(251,194,199,0.7)]"
onClick={() => {
setIsCreating(true);
setIsOpen(true);
}}
>
<PlusIcon />
</div>
<div class="absolute left-0 my-auto hidden flex-row space-x-3 opacity-0 transition-opacity duration-100 group-hover:opacity-100 md:flex">
<div
class="pointer-events-none flex aspect-square h-20 cursor-pointer flex-col items-center justify-center rounded-full bg-[rgba(206,232,251,0.7)] text-xl text-gray-600 shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] group-hover:pointer-events-auto hover:bg-[rgba(206,232,251,0.9)]"
onClick={(e) => {
e.stopPropagation();
if (!openSearchModal) setOpenFilterModal(false);
setOpenSearchModal(!openSearchModal);
}}
>
<MagnifyingGlassIcon class="size-12" />
</div>
<div class="pointer-events-none flex aspect-square h-20 cursor-pointer flex-col items-center justify-center rounded-full bg-[rgba(206,232,251,0.7)] text-xl text-gray-600 shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] group-hover:pointer-events-auto hover:bg-[rgba(206,232,251,0.9)]">
<FunnelIcon
class="size-12"
onClick={(e) => {
e.stopPropagation();
if (!openFilterModal) setOpenSearchModal(false);
setOpenFilterModal(!openFilterModal);
}}
/>
</div>
</div>
</div>
{openSearchModal && (
<div class="fixed bottom-28 z-50 w-full rounded-lg bg-white p-4 shadow-lg md:absolute md:right-[24rem] md:mx-0 md:w-80">
{/* Пофиксить разфокус при вводе */}
<div class="flex items-center gap-2">
<MagnifyingGlassIcon class="size-6 text-gray-400" />
<input
type="text"
class="w-full rounded-lg border-0 px-4 py-2 outline-none"
placeholder="Поиск"
ref={searchInputRef}
value={searchQuery}
onInput={(e) => {
setSearchQuery((e.target as HTMLInputElement).value);
}}
/>
<XMarkIcon
class="size-5 cursor-pointer text-gray-400 hover:text-gray-600"
onClick={() => {
setSearchQuery("");
setOpenSearchModal(false);
}}
/>
</div>
</div>
)}
{openFilterModal && (
<div class="fixed top-[50%] z-50 w-full rounded-[2rem] bg-white p-4 shadow-lg md:absolute md:top-auto md:right-[24rem] md:bottom-28 md:w-80">
<div class="flex flex-col gap-4">
<div class="text-center text-lg font-semibold">Фильтры</div>
<div class="flex flex-col gap-2">
<div class="flex flex-row gap-2">
<Checkbox
name="overdue"
checked={filterTags.overdue}
onChange={(e) => {
setFilterTags({ ...filterTags, overdue: e.target.checked! });
}}
pt={{
root: {
className: cn("cursor-pointer inline-flex relative select-none align-bottom", "w-6 h-6"),
},
input: {
className: cn(
"absolute appearance-none top-0 left-0 size-full p-0 m-0 opacity-0 z-10 outline-none cursor-pointer"
),
},
box: ({ props, context }: CheckboxPassThroughMethodOptions) => ({
className: cn(
"flex items-center justify-center",
"border-2 w-6 h-6 text-gray-600 rounded-lg transition-colors duration-200",
{
"border-gray-300 bg-white": !context.checked,
"border-blue-500 bg-blue-500": context.checked,
},
{
"hover:border-blue-500 focus:outline-none focus:outline-offset-0 focus:shadow-[0_0_0_0.2rem_rgba(191,219,254,1)]":
!props.disabled,
"cursor-default opacity-60": props.disabled,
}
),
}),
icon: {
className: "w-4 h-4 transition-all duration-200 text-white text-base",
},
}}
></Checkbox>
<label htmlFor="overdue" class="cursor-pointer">
Просроченные
</label>
</div>
<div class="relative">
<div
class={cn(
"flex cursor-pointer items-center justify-center gap-2 rounded-lg bg-[rgba(206,232,251,0.7)] px-4 py-2",
{
"bg-[rgba(206,232,251,0.9)]": filterTags.first,
}
)}
onClick={() => {
setOpenFirstList(!openFirstList);
setOpenSecondList(false);
}}
>
<BookOpenIcon class="size-5" />
<span>{filterTags.first || "Предмет"}</span>
</div>
{openFirstList && (
<div class="absolute top-full right-0 z-50 w-64 rounded-lg bg-white p-2 shadow-lg md:top-0 md:right-full md:mr-2">
{example_tags.first.map((tag) => (
<div
key={tag}
class="group flex cursor-pointer items-center gap-2 rounded-lg px-4 py-2 hover:bg-[rgba(206,232,251,0.3)]"
onClick={() => {
setFilterTags({ ...filterTags, first: tag });
setOpenFirstList(false);
}}
>
<div
class={cn("flex h-5 w-5 items-center justify-center rounded-full border-2", {
"border-[#FBC2C7] bg-[#FBC2C7]": filterTags.first === tag,
"border-[#D4D4D4]": filterTags.first !== tag,
})}
>
{filterTags.first === tag && <div class="h-2.5 w-2.5 rounded-full bg-white" />}
</div>
<span class="text-sm font-medium text-[#404040]">{tag}</span>
</div>
))}
</div>
)}
</div>
<div class="relative">
<div
class={cn(
"flex cursor-pointer items-center justify-center gap-2 rounded-lg bg-[rgba(206,232,251,0.7)] px-4 py-2",
{
"bg-[rgba(206,232,251,0.9)]": filterTags.second,
}
)}
onClick={() => {
setOpenSecondList(!openSecondList);
setOpenFirstList(false);
}}
>
<DocumentDuplicateIcon class="size-5" />
<span>{filterTags.second || "Задача"}</span>
</div>
{openSecondList && (
<div class="absolute top-full right-0 z-50 w-64 rounded-lg bg-white p-2 shadow-lg md:top-0 md:right-full md:mr-2">
{example_tags.second.map((tag) => (
<div
key={tag}
class="group flex cursor-pointer items-center gap-2 rounded-lg px-4 py-2 hover:bg-[rgba(206,232,251,0.3)]"
onClick={() => {
setFilterTags({ ...filterTags, second: tag });
setOpenSecondList(false);
}}
>
<div
class={cn("flex h-5 w-5 items-center justify-center rounded-full border-2", {
"border-[#FBC2C7] bg-[#FBC2C7]": filterTags.second === tag,
"border-[#D4D4D4]": filterTags.second !== tag,
})}
>
{filterTags.second === tag && <div class="h-2.5 w-2.5 rounded-full bg-white" />}
</div>
<span class="text-sm font-medium text-[#404040]">{tag}</span>
</div>
))}
</div>
)}
</div>
{(filterTags.first || filterTags.second || filterTags.overdue) && (
<button
class="mt-2 w-full rounded-lg bg-red-100 px-4 py-2 text-red-600 hover:bg-red-200"
onClick={() => {
setFilterTags({ first: "", second: "", overdue: false });
setOpenFirstList(false);
setOpenSecondList(false);
}}
>
Сбросить фильтры
</button>
)}
</div>
</div>
</div>
)}
</div>
);
};
export default withTitle(UrlsTitle.TASKS, ProfileTasks);

View File

@@ -1,3 +1,4 @@
import { stringToBoolean } from "@/utils/converter";
import { signal, Signal } from "@preact/signals"; import { signal, Signal } from "@preact/signals";
import { createContext, JSX } from "preact"; import { createContext, JSX } from "preact";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
@@ -5,14 +6,15 @@ import { useContext } from "preact/hooks";
interface AppContextValue { interface AppContextValue {
isLoggedIn: Signal<boolean>; isLoggedIn: Signal<boolean>;
} }
const ininitialValue = stringToBoolean(localStorage.getItem("loggedIn"));
const AppContext = createContext<AppContextValue>({ const AppContext = createContext<AppContextValue>({
isLoggedIn: signal(false), isLoggedIn: signal(ininitialValue),
}); });
const AppProvider = ({ children }: { children: JSX.Element }) => { const AppProvider = ({ children }: { children: JSX.Element }) => {
const value: AppContextValue = { const value: AppContextValue = {
isLoggedIn: signal(false), isLoggedIn: signal(ininitialValue),
}; };
return <AppContext.Provider value={value}>{children}</AppContext.Provider>; return <AppContext.Provider value={value}>{children}</AppContext.Provider>;

5
src/utils/converter.ts Normal file
View File

@@ -0,0 +1,5 @@
export const stringToBoolean = (value: string | null): boolean => {
if (value === "true") return true;
if (value === "false") return false;
return false;
};

View File

@@ -0,0 +1,26 @@
export interface StatusLevel {
name: string;
minPoints: number;
}
export const STATUS_LEVELS: StatusLevel[] = [
{ name: "Завтра точно начну", minPoints: 0 },
{ name: "Всё по плану (плана нет)", minPoints: 25 },
{ name: "Гений прокрастинации (но всё сделал)", minPoints: 50 },
{ name: "Хвостоуничтожитель 3000", minPoints: 75 },
{ name: "Легенда планирования", minPoints: 90 }
];
const POINTS_PER_TASK = 5;
export const calculatePoints = (completedTasks: number): number => {
return Math.min(completedTasks * POINTS_PER_TASK, 100);
};
export const getCurrentStatus = (points: number): string => {
const status = STATUS_LEVELS
.slice()
.reverse()
.find(level => points >= level.minPoints);
return status?.name || STATUS_LEVELS[0].name;
};