diff --git a/app/index.html b/app/index.html index e4b78ea..0f14b2c 100644 --- a/app/index.html +++ b/app/index.html @@ -4,10 +4,10 @@ - Vite + React + TS + Phonebook
- + \ No newline at end of file diff --git a/app/package-lock.json b/app/package-lock.json index 9fa27dd..0be928e 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -8,6 +8,8 @@ "name": "app", "version": "0.0.0", "dependencies": { + "@radix-ui/react-dialog": "^1.1.11", + "@radix-ui/react-label": "^2.1.4", "@radix-ui/react-slot": "^1.2.0", "@tailwindcss/vite": "^4.1.4", "axios": "^1.8.4", @@ -717,6 +719,11 @@ "node": ">= 8" } }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==" + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -732,6 +739,226 @@ } } }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.11.tgz", + "integrity": "sha512-yI7S1ipkP5/+99qhSI6nthfo/tR6bL6Zgxi/+1UO6qPa6UeM6nlafWcQ65vB4rU2XjgjMfMhI3k9Y5MztA62VQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.7", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.4", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.6", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.7.tgz", + "integrity": "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw==", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.4.tgz", + "integrity": "sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.4.tgz", + "integrity": "sha512-wy3dqizZnZVV4ja0FNnUhIWNwWdoldXrneEyUcVtLYDAt8ovGS4ridtMAOGgXBBIfggL4BOveVWsjXDORdGEQg==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.6.tgz", + "integrity": "sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", + "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", @@ -750,6 +977,86 @@ } } }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.40.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", @@ -1527,7 +1834,7 @@ "version": "19.1.2", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.2.tgz", "integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -1815,6 +2122,17 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2028,6 +2346,11 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2538,6 +2861,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -3414,6 +3745,72 @@ } } }, + "node_modules/react-remove-scroll": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -3636,8 +4033,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/tw-animate-css": { "version": "1.2.5", @@ -3715,6 +4111,47 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/vite": { "version": "6.2.6", "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz", diff --git a/app/package.json b/app/package.json index 2952c2e..c40a426 100644 --- a/app/package.json +++ b/app/package.json @@ -10,6 +10,8 @@ "preview": "vite preview" }, "dependencies": { + "@radix-ui/react-dialog": "^1.1.11", + "@radix-ui/react-label": "^2.1.4", "@radix-ui/react-slot": "^1.2.0", "@tailwindcss/vite": "^4.1.4", "axios": "^1.8.4", diff --git a/app/src/App.tsx b/app/src/App.tsx index a392c5e..482e1f4 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -1,17 +1,16 @@ -import { Button } from "@/components/ui/button" -import useT, {AutoTranslationProvider} from "@/contexts/AutoTranslationContext.tsx"; +import {PhonebookPage} from "@/pages/phonebook/phonebookPage.tsx"; +import {AutoTranslationProvider} from "@/contexts/AutoTranslationContext.tsx"; import {StorageProvider} from "@/contexts/StorageContext.tsx"; import {ThemeProvider} from "@/contexts/ThemeContext.tsx"; function App() { - const { _ } = useT(); return (
- +
diff --git a/app/src/components/phonebook/AddEntry.tsx b/app/src/components/phonebook/AddEntry.tsx new file mode 100644 index 0000000..0e51170 --- /dev/null +++ b/app/src/components/phonebook/AddEntry.tsx @@ -0,0 +1,77 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useState } from "react"; + +export function AddEntry({ onAdd }: { onAdd: (name: string, phone: string, email: string) => void }) { + const [name, setName] = useState(""); + const [phone, setPhone] = useState(""); + const [email, setEmail] = useState(""); + + const handleAdd = () => { + onAdd(name, phone, email); + setName(""); + setPhone(""); + setEmail(""); + }; + + return ( + + + + + + + Add new phone + +
+
+ + setName(e.target.value)} + className="col-span-3" + /> +
+
+ + setPhone(e.target.value)} + placeholder="+XXXXXXXXXXX" + className="col-span-3" + /> +
+
+ + setEmail(e.target.value)} + className="col-span-3" + /> +
+
+ + + +
+
+ ); +} diff --git a/app/src/components/phonebook/DeleteDialog.tsx b/app/src/components/phonebook/DeleteDialog.tsx new file mode 100644 index 0000000..f377003 --- /dev/null +++ b/app/src/components/phonebook/DeleteDialog.tsx @@ -0,0 +1,39 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogClose +} from "@/components/ui/dialog"; + +type DeleteDialogProps = { + onConfirm: () => void; +}; + +export function DeleteDialog({ onConfirm }: DeleteDialogProps) { + return ( + + + + + + + Delete phone + + Are you sure you want to delete this entry? + + + + + + + + + + + ); +} diff --git a/app/src/components/phonebook/EditDialog.tsx b/app/src/components/phonebook/EditDialog.tsx new file mode 100644 index 0000000..7750f34 --- /dev/null +++ b/app/src/components/phonebook/EditDialog.tsx @@ -0,0 +1,92 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogClose +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useState } from "react"; + +type EditDialogProps = { + name: string; + phone: string; + email: string; + onSave: (name: string, phone: string, email: string) => void; +}; + +export function EditDialog({ name, phone, email, onSave }: EditDialogProps) { + const [editedName, setEditedName] = useState(name); + const [editedPhone, setEditedPhone] = useState(phone); + const [editedEmail, setEditedEmail] = useState(email); + + const handleSave = () => { + onSave( + editedName !== undefined && editedName !== null ? editedName : name, + editedPhone !== undefined && editedPhone !== null ? editedPhone : phone, + editedEmail !== undefined && editedEmail !== null ? editedEmail : email + ); + }; + + return ( + + + + + + + Edit + + Make changes here. Click save when you're done. + + +
+
+ + setEditedName(e.target.value)} + className="col-span-3" + /> +
+
+ + setEditedPhone(e.target.value)} + className="col-span-3" placeholder="+XXXXXXXXXXX" + /> +
+
+ + setEditedEmail(e.target.value)} + className="col-span-3" + /> +
+
+ + + + + + +
+
+ ); +} diff --git a/app/src/components/phonebook/EntryCard.tsx b/app/src/components/phonebook/EntryCard.tsx new file mode 100644 index 0000000..64cb79e --- /dev/null +++ b/app/src/components/phonebook/EntryCard.tsx @@ -0,0 +1,41 @@ +import { DeleteDialog } from "./DeleteDialog"; +import { EditDialog } from "./EditDialog"; + +type EntryCardProps = { + name: string; + phone: string; + email: string; + onEdit: (name: string, phone: string, email: string) => void; + onDelete: () => void; +}; + +export function EntryCard({ name, phone, email, onEdit, onDelete }: EntryCardProps) { + const handleEditSave = (editedName: string, editedPhone: string, editedEmail: string) => { + onEdit(editedName, editedPhone, editedEmail); + }; + + const handleDeleteConfirm = () => { + onDelete(); + }; + + return ( +
+
+

{name}

+

{phone}

+

{email}

+
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/app/src/components/ui/alert.tsx b/app/src/components/ui/alert.tsx new file mode 100644 index 0000000..1421354 --- /dev/null +++ b/app/src/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/app/src/components/ui/card.tsx b/app/src/components/ui/card.tsx new file mode 100644 index 0000000..d05bbc6 --- /dev/null +++ b/app/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/app/src/components/ui/dialog.tsx b/app/src/components/ui/dialog.tsx new file mode 100644 index 0000000..981e999 --- /dev/null +++ b/app/src/components/ui/dialog.tsx @@ -0,0 +1,133 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + {children} + + + Close + + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/app/src/components/ui/input.tsx b/app/src/components/ui/input.tsx new file mode 100644 index 0000000..03295ca --- /dev/null +++ b/app/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/app/src/components/ui/label.tsx b/app/src/components/ui/label.tsx new file mode 100644 index 0000000..ef7133a --- /dev/null +++ b/app/src/components/ui/label.tsx @@ -0,0 +1,22 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" + +import { cn } from "@/lib/utils" + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Label } diff --git a/app/src/index.css b/app/src/index.css index 1199c18..967f0a9 100644 --- a/app/src/index.css +++ b/app/src/index.css @@ -1,9 +1,7 @@ @import "tailwindcss"; @import "tw-animate-css"; -@custom-variant dark (&:is(.dark *)); - -@theme inline { +@theme { --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); @@ -39,9 +37,6 @@ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); -} - -:root { --radius: 0.625rem; --background: oklch(1 0 0); --foreground: oklch(0.141 0.005 285.823); @@ -112,9 +107,11 @@ @layer base { * { - @apply border-border outline-ring/50; + border: var(--color-border); + outline: 50% solid var(--color-ring); } body { - @apply bg-background text-foreground; + background-color: var(--color-background); + color: var(--color-foreground); } } diff --git a/app/src/pages/phonebook/phonebookPage.tsx b/app/src/pages/phonebook/phonebookPage.tsx new file mode 100644 index 0000000..3fb3e61 --- /dev/null +++ b/app/src/pages/phonebook/phonebookPage.tsx @@ -0,0 +1,167 @@ +import { useEffect, useState } from "react"; +import { + Alert, + AlertDescription, + AlertTitle, +} from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { EntryCard } from "@/components/phonebook/EntryCard"; +import { AddEntry } from "@/components/phonebook/AddEntry"; +import { fetchEntries, addEntry, editEntry, deleteEntry } from "@/services/phonebookService"; +import { Input } from "@/components/ui/input"; +import { Terminal } from "lucide-react"; + +export function PhonebookPage() { + const [entries, setEntries] = useState<{ + id: string; + name: string; + phone: string; + email: string; + }[]>([]); + const [searchQuery, setSearchQuery] = useState(""); + const [error, setError] = useState(null); + const [showError, setShowError] = useState(false); + const [isDarkMode, setIsDarkMode] = useState(false); + + useEffect(() => { + const loadEntries = async () => { + try { + const data = await fetchEntries(); + const flatData = Object.values(data).flat() as { + id: string; + name: string; + phone: string; + email: string; + }[]; + setEntries(flatData); + } catch (err) { + handleError("Failed to load entries."); + } + }; + + loadEntries(); + }, []); + + useEffect(() => { + if (isDarkMode) { + document.documentElement.classList.add("dark"); + } else { + document.documentElement.classList.remove("dark"); + } + }, [isDarkMode]); + + const handleError = (message: string) => { + setError(message); + setShowError(true); + setTimeout(() => { + setShowError(false); + setTimeout(() => { + setError(null); + }, 500); + }, 4000); + }; + + const handleAddEntry = async (name: string, phone: string, email: string) => { + if (!name || !phone || !email) { + handleError("All fields are required."); + return; + } + if (!/^\+\d{11}$/.test(phone)) { + handleError("Phone number must be written in the format +XXXXXXXXXXX"); + return; + } + try { + const newEntry = await addEntry(name, phone, email); + setEntries((prev) => [...prev, newEntry]); + } catch (err) { + handleError("Failed to add entry."); + } + }; + + const handleEditEntry = async (id: string, name: string, phone: string, email: string) => { + if (!name || !phone || !email) { + handleError("All fields are required."); + return; + } + if (!/^\+\d{11}$/.test(phone)) { + handleError("Phone number must be written in the format +XXXXXXXXXXX"); + return; + } + try { + const updatedEntry = await editEntry(id, name, phone, email); + setEntries((prev) => + prev.map((entry) => (entry.id === id ? updatedEntry : entry)) + ); + } catch (err) { + handleError("Failed to edit entry."); + } + }; + + const handleDeleteEntry = async (id: string) => { + try { + await deleteEntry(id); + setEntries((prev) => prev.filter((entry) => entry.id !== id)); + } catch (err) { + handleError("Failed to delete entry."); + } + }; + + const filteredEntries = searchQuery + ? entries.filter((entry) => + entry.name.toLowerCase().includes(searchQuery.toLowerCase()) + ) + : entries; + + return ( +
+
+

My Phonebook

+
+ + +
+
+
+ setSearchQuery(e.target.value)} + className="w-full max-w-md mx-auto" + /> +
+
+ +
+ {error && ( +
+ + + Something went wrong + {error} + +
+ )} + {entries.length > 0 && filteredEntries.length === 0 ? ( +
+

No matching entries found.

+
+ ) : ( +
+ {filteredEntries.map((entry) => ( + handleEditEntry(entry.id, name, phone, email)} + onDelete={() => handleDeleteEntry(entry.id)} + /> + ))} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/app/src/services/phonebookService.tsx b/app/src/services/phonebookService.tsx new file mode 100644 index 0000000..85672a1 --- /dev/null +++ b/app/src/services/phonebookService.tsx @@ -0,0 +1,21 @@ +import { jsonRpcRequest } from "@/utils/jsonRpcClient"; + +export async function addEntry(name: string, phone: string, email: string) { + return await jsonRpcRequest("AddContact", { name, phone, email}); +} + +export async function editEntry(id: string, name: string, phone: string, email: string) { + return await jsonRpcRequest("UpdateContact", {id, name, phone, email}); +} + +export async function deleteEntry(id: string) { + return await jsonRpcRequest("DeleteContact", { id }); +} + +export async function GetByName() { + return await jsonRpcRequest("GetByName", {name}); +} + +export async function fetchEntries() { + return await jsonRpcRequest("GetAllContacts", {}); +} \ No newline at end of file diff --git a/app/src/utils/jsonRpcClient.tsx b/app/src/utils/jsonRpcClient.tsx new file mode 100644 index 0000000..7bb1942 --- /dev/null +++ b/app/src/utils/jsonRpcClient.tsx @@ -0,0 +1,23 @@ +import axios from "axios"; + +const axiosInstance = axios.create({ + baseURL: "https://stage.dnp-project.ru/rpc", + headers: { + "Content-Type": "application/json", + }, +}); + +export async function jsonRpcRequest(method: string, params: any) { + try { + const response = await axiosInstance.post("", { + jsonrpc: "2.0", + method, + params, + id: Date.now(), + }); + return response.data.result; + } catch (error: any) { + console.error("JSON-RPC Error:", error.response?.data || error.message); + throw error; + } +} \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 0f0088f..260d170 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,40 +1,34 @@ -## Стек +## Stack react, vite, typescript (SWC), shadcn, axios, i18n -## Структура проекта -app - сам фронтенд +## Project structure +app - the frontend code itself -devops - настройка и конфиг для девопса (не трогать) +devops - devops config files -scripts - готовые скрипты для упрощения жизни (не трогать) +scripts - scripts to make life easier -docs - документация +docs - documentation ## Структура фронтенда -public/locales/ - для локалей, автогенерится (не трогать) +public/locales/ - automatically generated locales code -src/client/ - автогенерация кода для клиента (не трогать) +src/client/ - automatically generated client code -src/components/ - компоненты shadCN, можно стилизовать под собственные нужды +src/components/ - shadCN and other stylized components -src/contexts - контексты +src/contexts - contexts -src/pages/ - отдельные страницы +src/pages/ - all page files -## Связь с бекендом +## Backend connection -У нас в итоге будет использоваться json rpc, тк он живет на http1 и может нормально использоваться в вебе. Axios дает полную поддержку по работе с json rpc, так что проблем быть не должно +We ended up using jsonRPC since it works on http1 and can be used normally on the web. Axios provides full support for working with jsonRPC, so no problems should arise because of this -Еще ты можешь заметить, что есть скрипт автогенерации REST клиента на основе openapi - нам это вероятно не пригодится, все зависит от глубины имплементации проекта, но лишним точно не будет + в итоговую сборку скрипты не входят, так что это ни на что не повлияет +There is a script for auto-generating a REST client based on openapi for future project updates but it is not included in the final build, so this will not affect anything -## Полезное +## Workflow -Если ты работаешь на WebStorm, то установи себе easy i18n. Это дает нам возможность быстро менять язык у приложения и тд. После установки у тебя появится вкладка для i18n, по настройке я помогу +There is a develop and master branch. While working on the project we only committed to the dev branch, the master branch was only updated via pull requests from dev to test all features first and keep the master version stable. -## Воркфлоу - -Вряд ли кому-то нужны все эти игрища с GitFlow и тд, поэтому все будет просто - будет две ветки - dev и master. В dev все время можно коммитить, а в master только Pull Request'ами из deva. В чем разница - в мастере должна быть только полностью стабильная версия. На серваке я все так настрою, что у нас будет следующая схема: - -https://\.\/ - стабильная версия, которая будет собираться с master'а - -https://dev.\.\/ - версия, которая будет собираться с dev'а +The develop version is hosted on stage.dnp-project.ru and the main version is hosted on dnp-project.ru