40 Commits

Author SHA1 Message Date
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
34 changed files with 1066 additions and 153 deletions

View File

@@ -4,6 +4,7 @@
"": { "": {
"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",
@@ -11,6 +12,9 @@
"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.0.17",
@@ -71,6 +75,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 +153,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 +277,10 @@
"@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=="],
"@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 +353,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 +367,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 +533,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 +557,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 +599,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 +677,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=="],
@@ -741,6 +779,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,6 +9,7 @@
"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",
@@ -16,6 +17,9 @@
"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.0.17"

View File

@@ -1,28 +1,46 @@
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 "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 { route } = useLocation(); const { route } = useLocation();
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>
<LocationProvider> <PrimeReactProvider
<ErrorBoundary> value={{
<Router> unstyled: true,
<Route path="/" component={HomePage} /> // pt: Tailwind,
<Route path="/login" component={LoginPage} /> // ptOptions: { mergeProps: true, mergeSections: true, classNameMergeFunction: cn },
<Route path="/profile/*" component={lazy(() => import("./pages/profile"))} /> }}
<Route default component={() => <h1>404</h1>} /> >
</Router> <LocationProvider>
</ErrorBoundary> <ErrorBoundary>
</LocationProvider> <Router>
<Route path="/" component={HomePage} />
<Route path="/login" component={LoginPage} />
<Route path="/profile/*" component={lazy(() => import("./pages/profile"))} />
<Route default component={() => <Page404 />} />
</Router>
</ErrorBoundary>
</LocationProvider>
</PrimeReactProvider>
</AppProvider> </AppProvider>
); );
} }

View File

@@ -0,0 +1,202 @@
import { cn } from "@/utils/class-merge";
import { ClockIcon } from "@heroicons/react/24/outline";
import { FunctionComponent } from "preact";
import { Dispatch, StateUpdater, useState } from "preact/hooks";
import { Calendar, CalendarPassThroughMethodOptions } from "primereact/calendar";
import { FormEvent } from "primereact/ts-helpers";
import Button from "./ui/Button";
import ModalWindow from "./ui/Modal";
interface ModalCalendarProps {
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 }) => {
const [showTime, setShowTime] = useState(false);
return (
<ModalWindow
isOpen={isOpen}
setIsOpen={setIsOpen}
onClose={() => {
onClose!();
setShowTime(false);
}}
className="md:h-[40rem] md:w-[30rem]"
zIndex={60}
>
<div class="w-full flex-1 self-start">
<Calendar
inline
onChange={onChange}
value={value}
hourFormat="24"
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>
);
};
export default ModalCalendar;

View File

@@ -1,7 +1,26 @@
import { cn } from "@/utils/class-merge"; import { cn } from "@/utils/class-merge";
import { BackwardIcon } from "@heroicons/react/24/solid";
import { FunctionComponent, h } from "preact"; import { FunctionComponent, h } from "preact";
import { useState } from "preact/hooks"; 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 MarkedDateType = "event" | "holiday" | "important" | string;
type MarkedDates = Record<string, MarkedDateType>; type MarkedDates = Record<string, MarkedDateType>;
@@ -42,7 +61,7 @@ const BigCalendar: FunctionComponent<BigCalendarProps> = ({
const getFirstDayOfMonth = (year: number, month: number): number => { const getFirstDayOfMonth = (year: number, month: number): number => {
const day = new Date(year, month, 1).getDay(); const day = new Date(year, month, 1).getDay();
return day === 0 ? 6 : day - 1; return day === 0 ? 6 : day;
}; };
const handlePrevMonth = (): void => { const handlePrevMonth = (): void => {
@@ -58,7 +77,7 @@ const BigCalendar: FunctionComponent<BigCalendarProps> = ({
setCurrentDate(new Date(date.getFullYear(), date.getMonth(), 1)); setCurrentDate(new Date(date.getFullYear(), date.getMonth(), 1));
} }
setSelectedDate(date); setSelectedDate(date);
onDateSelect?.(date); onDateSelect(date);
}; };
const isDateMarked = (date: Date): MarkedDateType | undefined => { const isDateMarked = (date: Date): MarkedDateType | undefined => {
@@ -68,7 +87,7 @@ const BigCalendar: FunctionComponent<BigCalendarProps> = ({
const getLastDayOfMonth = (year: number, month: number): number => { const getLastDayOfMonth = (year: number, month: number): number => {
const day = new Date(year, month + 1, 0).getDay(); const day = new Date(year, month + 1, 0).getDay();
return day === 0 ? 6 : day - 1; return day === 0 ? 6 : day;
}; };
const renderDays = (): h.JSX.Element[] => { const renderDays = (): h.JSX.Element[] => {
@@ -82,12 +101,11 @@ const BigCalendar: FunctionComponent<BigCalendarProps> = ({
const days: h.JSX.Element[] = []; const days: h.JSX.Element[] = [];
// Дни предыдущего месяца // Дни предыдущего месяца
//TODO: work on click on 31 march
const prevMonthDays = getDaysInMonth(year, month - 1); const prevMonthDays = getDaysInMonth(year, month - 1);
const daysFromPrevMonth = firstDayOfMonth === 0 ? 6 : firstDayOfMonth; const daysFromPrevMonth = firstDayOfMonth === 0 ? 6 : firstDayOfMonth;
for (let i = daysFromPrevMonth - 1; i >= 0; i--) { for (let i = daysFromPrevMonth - 1; i >= 0; i--) {
const day = prevMonthDays - i; const day = prevMonthDays - i;
const date = new Date(year, month - 1, day + 1); const date = new Date(year, month - 1, day);
const dateStr = date.toISOString().split("T")[0]; const dateStr = date.toISOString().split("T")[0];
const isSelected = selectedDate?.toISOString().split("T")[0] === dateStr; const isSelected = selectedDate?.toISOString().split("T")[0] === dateStr;
const markType = isDateMarked(date); const markType = isDateMarked(date);
@@ -120,7 +138,7 @@ const BigCalendar: FunctionComponent<BigCalendarProps> = ({
// Дни текущего месяца // Дни текущего месяца
for (let day = 1; day <= daysInMonth; day++) { for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(year, month, day + 1); const date = new Date(year, month, day);
const dateStr = date.toISOString().split("T")[0]; const dateStr = date.toISOString().split("T")[0];
const isSelected = selectedDate?.toISOString().split("T")[0] === dateStr; const isSelected = selectedDate?.toISOString().split("T")[0] === dateStr;
const markType = isDateMarked(date); const markType = isDateMarked(date);
@@ -164,9 +182,9 @@ const BigCalendar: FunctionComponent<BigCalendarProps> = ({
} }
// Дни следующего месяца // Дни следующего месяца
const daysToAdd = 6 - (lastDayOfMonth === 6 ? 4 : lastDayOfMonth); const daysToAdd = 6 - (lastDayOfMonth === 6 ? 3 : lastDayOfMonth);
for (let day = 1; day <= daysToAdd; day++) { for (let day = 1; day <= daysToAdd; day++) {
const date = new Date(year, month + 1, day + 1); const date = new Date(year, month + 1, day);
const dateStr = date.toISOString().split("T")[0]; const dateStr = date.toISOString().split("T")[0];
const isSelected = selectedDate?.toISOString().split("T")[0] === dateStr; const isSelected = selectedDate?.toISOString().split("T")[0] === dateStr;
const markType = isDateMarked(date); const markType = isDateMarked(date);
@@ -201,7 +219,7 @@ const BigCalendar: FunctionComponent<BigCalendarProps> = ({
}; };
return ( return (
<div className={`flex flex-col ${className}`}> <div className={`flex w-full flex-col ${className}`}>
<div className="mb-4 flex items-center justify-between px-2"> <div className="mb-4 flex items-center justify-between px-2">
<button <button
onClick={handlePrevMonth} onClick={handlePrevMonth}
@@ -233,7 +251,6 @@ const BigCalendar: FunctionComponent<BigCalendarProps> = ({
</svg> </svg>
</button> </button>
</div> </div>
<div className="mb-2 grid grid-cols-7 gap-1 text-center text-sm font-medium text-gray-500"> <div className="mb-2 grid grid-cols-7 gap-1 text-center text-sm font-medium text-gray-500">
{dayNames.map((day) => ( {dayNames.map((day) => (
<div key={day} className="flex h-10 items-center justify-center"> <div key={day} className="flex h-10 items-center justify-center">
@@ -241,8 +258,8 @@ const BigCalendar: FunctionComponent<BigCalendarProps> = ({
</div> </div>
))} ))}
</div> </div>
<div className="grid flex-1 grid-cols-7 gap-1">{renderDays()}</div> <div className="grid flex-1 grid-cols-7 gap-1">{renderDays()}</div>
<BackButton selectedDate={currentDate} onClick={() => setCurrentDate(new Date())} />
</div> </div>
); );
}; };

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 right-0 bottom-0 left-0 flex min-h-auto flex-row items-center gap-1 border-t border-t-gray-500 bg-white px-1 py-5 md:sticky md:top-0 md:right-0 md:bottom-auto md:left-auto md:min-h-screen md:w-auto md:flex-col md:gap-4 md:border-t-0 md:border-l md:border-l-gray-500 md:px-5 md:py-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,4 +1,6 @@
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";
@@ -6,9 +8,10 @@ import classes from "./menu.module.scss";
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 { route, path } = useLocation(); const { route, path } = useLocation();
const active = path === link; const active = path === link;
const menuItemClasses = tv({ const menuItemClasses = tv({
@@ -16,7 +19,7 @@ const MenuItem: FunctionComponent<MenuItemProps> = ({ title, link }: MenuItemPro
variants: { variants: {
activity: { activity: {
active: "bg-gray-200", active: "bg-gray-200",
inactive: "bg-gray-100", inactive: "bg-white",
}, },
}, },
defaultVariants: { defaultVariants: {
@@ -25,36 +28,70 @@ const MenuItem: FunctionComponent<MenuItemProps> = ({ title, link }: MenuItemPro
}); });
return ( return (
<div class={menuItemClasses({ activity: active ? "active" : "inactive" })} onClick={() => route(link, true)}> <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 { route, path } = useLocation();
//TODO: Move styles to scss module
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">статус</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 class={classes.menu_container}> <div id={classes.container}>
{menu_items.map(({ title, link }) => ( <Avatar />
<MenuItem title={title} link={link} /> <div class={classes.menu_container}>
))} {menu_items.map(({ title, link, icon }) => (
<MenuItem title={title} link={link} icon={icon} />
))}
</div>
</div> </div>
); );
}; };

View File

@@ -0,0 +1,5 @@
@reference "../index.scss";
.task {
@apply 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];
}

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

@@ -0,0 +1,45 @@
import { FunctionComponent } from "preact";
import { tv } from "tailwind-variants";
import classes from "./task.module.scss";
interface TaskProps {
name: string;
checked?: boolean;
onClick?: () => void;
}
const taskStyle = tv({
base: "flex aspect-square h-full flex-col items-center justify-center rounded-full border",
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 = () => {} }: TaskProps) => {
return (
// Временное действие для тестирования
<button onClick={onClick} class="w-[95%]">
<div class={classes.task}>
<div class={taskStyle({ checked })}>
<p class={markStyle({ checked })}></p>
</div>
{name}
</div>
</button>
);
};
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

@@ -5,20 +5,29 @@ 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)]",
}, },
}, },
}); });
interface ButtonProps { interface ButtonProps {
color?: "primary" | "secondary"; color?: "primary" | "secondary" | "red";
onClick: () => void; onClick?: () => void;
className?: string;
type?: "button" | "submit";
} }
const Button: FunctionComponent<ButtonProps> = ({ children, onClick, color = "primary" }) => { const Button: FunctionComponent<ButtonProps> = ({
children,
onClick = () => {},
color = "primary",
className = "",
type = "button",
}) => {
return ( return (
<button type="button" class={button({ color: color })} onClick={onClick}> <button type={type} class={button({ color: color, class: className })} onClick={onClick}>
{children} {children}
</button> </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

@@ -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";
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

@@ -3,3 +3,19 @@
:root { :root {
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,9 +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 { .login_card_name {
@apply text-center text-2xl font-semibold; @apply mb-8 text-center text-2xl font-semibold;
} }

View File

@@ -1,29 +1,76 @@
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 { useState } from "preact/hooks";
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 { route } = useLocation(); const { route } = useLocation();
return ( const [passwordError, setPasswordError] = useState("");
const login: SubmitHandler<ILoginForm> = async (data) => {
console.log(data);
if (data.login !== testUser.login || data.password !== testUser.password) {
setError("login", { message: "Неверный" }); //TODO: не показывает ошибку
return;
}
isLoggedIn.value = true;
localStorage.setItem("loggedIn", "true");
route("/profile/tasks", true);
};
const { control, handleSubmit, formState, setError } = useForm({
defaultValues: {
login: "",
password: "",
},
mode: "onChange",
});
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}>
<p class={classes.login_card_name}>Антихвост</p> <p class={classes.login_card_name}>Антихвост</p>
<Input placeholder="Login" textAlign="center" /> <form onSubmit={handleSubmit((data) => login(data))}>
<Input isPassword placeholder="Password" textAlign="center" /> <Controller
<Button name="login"
onClick={() => { control={control}
isLoggedIn.value = true; rules={{
route("/profile/settings", true); required: "Введите логин",
}} }}
> render={({ field }) => (
Login <Input placeholder="Логин" textAlign="center" error={formState.errors.login?.message} {...field} />
</Button> )}
/>
<Controller
name="password"
control={control}
rules={{
required: "Введите пароль",
}}
render={({ field }) => (
<Input placeholder="Пароль" textAlign="center" type="password" error={passwordError} {...field} />
)}
/>
<Button type="submit" color="secondary" className="w-full">
Войти
</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 { lazy, 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 h-screen flex-col md:flex-row"> if (!isLoggedIn.value) route("/login", true);
<div class="flex-1 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={lazy(() => import("./calendar"))} />
<Route <Route
default default
component={() => { component={() => {
@@ -92,10 +24,12 @@ const ProfilePage: FunctionComponent = () => {
/> />
</Router> </Router>
</div> </div>
<div className="md:sticky md:top-0 md:h-screen"> <div id={ids.menu_container}>
<Menu /> <Menu />
</div> </div>
</div> </div>
) : (
<p>Redirecting...</p>
); );
}; };

View File

@@ -0,0 +1,17 @@
import BigCalendar from "@/components/calendar";
import { withTitle } from "@/constructors/Component";
import { UrlsTitle } from "@/enums/urls";
import { FunctionComponent } from "preact";
const ProfileCalendar: FunctionComponent = () => {
const onDateSelect = (date: Date) => {
console.log(date);
};
return (
<div class="flex w-full flex-col items-center">
<BigCalendar onDateSelect={onDateSelect} />
</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,55 @@
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 { FunctionComponent } from "preact";
import { useLocation } from "preact-iso";
import classes from "./profile_settings.module.scss";
const ProfileSettings: FunctionComponent = () => {
const { isLoggedIn } = useAppContext();
const { route } = useLocation();
const status = 12;
const maxStatus = 100;
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">Статус</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>Сменить тему</Button>
<Button>Настройки</Button>
<Button
color="secondary"
onClick={() => {
isLoggedIn.value = false;
localStorage.setItem("loggedIn", "false");
route("/login", true);
}}
>
Выйти
</Button>
</div>
</div>
</div>
</div>
);
};
export default withTitle(UrlsTitle.PROFILE, ProfileSettings);

View File

@@ -0,0 +1,8 @@
export interface ITask {
id: number;
name: string;
checked: boolean;
date: Date;
description: string;
tags: 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 w-full text-3xl font-semibold md:mb-12 md:text-5xl;
}
.tasks_container {
@apply flex w-full flex-col items-center gap-3 md:items-start md:gap-10;
}

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

@@ -0,0 +1,261 @@
import Task from "@/components/task";
import ModalCalendar from "@/components/ModalCalendar";
import Button from "@/components/ui/Button";
import ModalWindow from "@/components/ui/Modal";
import { withTitle } from "@/constructors/Component";
import { UrlsTitle } from "@/enums/urls";
import { PlusIcon } from "@heroicons/react/20/solid";
import {
BookmarkIcon,
BookOpenIcon,
CalendarDaysIcon,
DocumentDuplicateIcon,
FunnelIcon,
MagnifyingGlassIcon,
PencilIcon,
} from "@heroicons/react/24/outline";
import { FunctionComponent } from "preact";
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { Nullable } from "primereact/ts-helpers";
import { ITask } from "./profile_tasks.dto";
import classes from "./profile_tasks.module.scss";
const example_tasks: ITask[] = [
{
checked: false,
date: new Date(),
description: "test",
id: 1,
name: "test1",
tags: ["Программирование", "Лабораторная работа"],
},
{
checked: true,
date: new Date(2025, 6, 2),
description: "test2",
id: 3,
name: "test3",
tags: ["Информатика", "Практическая работа"],
},
{
checked: false,
date: new Date(2025, 5, 1),
description: "test3",
id: 2,
name: "test2",
tags: ["Математика", "Домашнее задание"],
},
];
const ProfileTasks: FunctionComponent = () => {
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)
: example_tasks;
init_tasks.forEach((task) => {
task.date = new Date(task.date);
});
const [tasks, setTasks] = useState<ITask[]>(init_tasks);
useEffect(() => {
localStorage.setItem("tasks", JSON.stringify(tasks));
}, [tasks]);
const [openModal, setIsOpen] = useState(false);
const [openModalCalendar, setOpenModalCalendar] = useState(false);
const [isEdit, setIsEdit] = useState(false);
const [isCreating, setIsCreating] = useState(false);
const [editContent, setEditContent] = useState<ITask | null>(null);
const taskNameRef = useRef<HTMLInputElement>(null);
const taskDescriptionRef = useRef<HTMLTextAreaElement>(null);
const [calendarDate, setCalendarDate] = useState<Nullable<Date>>();
return (
<div class={classes.container}>
<ModalCalendar
isOpen={openModalCalendar}
setIsOpen={setOpenModalCalendar}
onClose={() => {
if (isEdit) setCalendarDate(null);
}}
onChange={(e) => isCreating && setCalendarDate(e.value)}
value={calendarDate!}
/>
<ModalWindow
isOpen={openModal}
setIsOpen={setIsOpen}
onClose={() => {
setIsEdit(false);
setEditContent(null);
setIsCreating(false);
setCalendarDate(null);
}}
>
{isEdit && editContent && (
<div class="flex h-full w-full flex-col items-start justify-between">
<div class="flex w-full flex-row items-start justify-between">
<div class="flex flex-col">
<p class="text-2xl">{editContent.name}</p>
<p>{editContent.description}</p>
</div>
<div className="flex cursor-pointer flex-col items-center gap-3">
<PencilIcon class="size-6" />
<p class="text-[0.7rem]">Редактировать</p>
</div>
</div>
<div class="flex flex-col items-center gap-6 self-center md:flex-row md:justify-start md:self-start">
<div
class="flex h-full flex-row items-center gap-1 rounded-2xl bg-[rgba(251,194,199,0.38)] px-2 py-1"
onClick={() => {
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(editContent.date)}
</p>
</div>
<div class="flex h-full flex-col items-start gap-1 rounded-2xl bg-[rgba(251,194,199,0.38)] px-4 py-2">
<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>
</div>
)}
{isCreating && (
<div class="flex h-full w-full flex-col items-start justify-between">
<div class="flex w-full flex-row items-start justify-between">
<div class="me-4 flex flex-1 flex-col gap-1">
<input class="text-2xl outline-0" maxLength={20} placeholder="Название" ref={taskNameRef} />
<textarea
class="h-[5rem] w-full resize-none outline-0"
maxLength={200}
placeholder="Описание"
ref={taskDescriptionRef}
/>
</div>
<CalendarDaysIcon
class="size-10 cursor-pointer"
onClick={() => {
setOpenModalCalendar(true);
setCalendarDate(calendarDate ?? new Date());
}}
/>
<BookmarkIcon class="ms-4 size-10 cursor-pointer" />
</div>
<div className="mb-8 flex h-16 flex-col items-center gap-6 self-center md:mb-0 md:flex-row">
<Button
className="text-sm"
onClick={() => {
setIsOpen(false);
}}
>
Отмена
</Button>
<Button
color="red"
onClick={() => {
if (taskNameRef.current && taskDescriptionRef.current) {
if (!taskNameRef.current.value || !taskDescriptionRef.current.value) {
alert("Заполните все поля");
return;
}
if (!calendarDate) {
alert("Заполните дату и время");
return;
}
const task: ITask = {
id: tasks.length + 1,
name: taskNameRef.current.value,
description: taskDescriptionRef.current.value,
date: calendarDate,
checked: false,
tags: ["Математика", "Домашнее задание"],
};
setTasks([...tasks, task]);
setIsOpen(false);
setCalendarDate(null);
}
}}
>
Добавить задачу
</Button>
</div>
</div>
)}
</ModalWindow>
{tasks.length > 0 ? (
<>
<div class={classes.header}>Сегодня: {getDate}</div>
<div class={classes.tasks_container}>
{tasks
.sort((a, b) => b.date.getTime() - a.date.getTime())
.map((task) => (
<Task
name={task.name}
key={task.id}
checked={task.checked}
onClick={() => {
setIsOpen(true);
setIsEdit(true);
setEditContent(task);
}}
/>
))}
</div>
<div class="group fixed right-[22rem] bottom-4 hidden flex-row items-center justify-start space-x-3 overflow-x-hidden py-2 md:flex">
<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 flex flex-row space-x-3 opacity-0 transition-opacity duration-100 group-hover:opacity-100">
<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)]">
<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" />
</div>
</div>
</div>
</>
) : (
<>
<div class="flex w-full flex-1 flex-col items-center justify-center text-2xl">Начни уже сегодня!</div>
<div class="fixed right-[22rem] bottom-4 hidden flex-row items-center justify-start overflow-x-hidden py-2 md:flex">
<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 hover:bg-[rgb(251,194,199,0.7)]"
onClick={() => {
setIsCreating(true);
setIsOpen(true);
}}
>
<PlusIcon />
</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;
};