diff --git a/package-lock.json b/package-lock.json index 65f14c2..e86e1ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,8 @@ "react-leaflet": "^4.2.1", "react-router": "^7.0.2", "react-router-dom": "^7.0.2", - "react-transition-group": "^4.4.5" + "react-transition-group": "^4.4.5", + "react-use": "^17.5.1" }, "devDependencies": { "@eslint/js": "^9.15.0", @@ -962,8 +963,7 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -1790,6 +1790,11 @@ "integrity": "sha512-9oSxFzDCT2Rj6DfcHF8G++jxBKS7mBqXl5xrRW+Kbvjry6Uduya2iiwqHPhVXpasAVMBYKkEPGgKhd3+/HZ6xA==", "dev": true }, + "node_modules/@types/js-cookie": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz", + "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2076,6 +2081,11 @@ "vite": "^4 || ^5 || ^6" } }, + "node_modules/@xobotyi/scrollbar-width": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz", + "integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==" + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -2437,6 +2447,14 @@ "node": ">=18" } }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -2477,6 +2495,26 @@ "node": ">= 8" } }, + "node_modules/css-in-js-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", + "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", + "dependencies": { + "hyphenate-style-name": "^1.0.3" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2601,6 +2639,14 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dependencies": { + "stackframe": "^1.3.4" + } + }, "node_modules/esbuild": { "version": "0.24.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", @@ -2837,8 +2883,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -2880,6 +2925,16 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-shallow-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", + "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==" + }, + "node_modules/fastest-stable-stringify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz", + "integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -3110,6 +3165,11 @@ "node": ">= 0.4" } }, + "node_modules/hyphenate-style-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3144,6 +3204,14 @@ "node": ">=0.8.19" } }, + "node_modules/inline-style-prefixer": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz", + "integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==", + "dependencies": { + "css-in-js-utils": "^3.1.0" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -3246,6 +3314,11 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3403,6 +3476,11 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3463,6 +3541,25 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nano-css": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.6.2.tgz", + "integrity": "sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "css-tree": "^1.1.2", + "csstype": "^3.1.2", + "fastest-stable-stringify": "^2.0.2", + "inline-style-prefixer": "^7.0.1", + "rtl-css-js": "^1.16.1", + "stacktrace-js": "^2.0.2", + "stylis": "^4.3.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, "node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -4010,6 +4107,40 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-universal-interface": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz", + "integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==", + "peerDependencies": { + "react": "*", + "tslib": "*" + } + }, + "node_modules/react-use": { + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/react-use/-/react-use-17.5.1.tgz", + "integrity": "sha512-LG/uPEVRflLWMwi3j/sZqR00nF6JGqTTDblkXK2nzXsIvij06hXl1V/MZIlwj1OKIQUtlh1l9jK8gLsRyCQxMg==", + "dependencies": { + "@types/js-cookie": "^2.2.6", + "@xobotyi/scrollbar-width": "^1.9.5", + "copy-to-clipboard": "^3.3.1", + "fast-deep-equal": "^3.1.3", + "fast-shallow-equal": "^1.0.0", + "js-cookie": "^2.2.1", + "nano-css": "^5.6.2", + "react-universal-interface": "^0.6.2", + "resize-observer-polyfill": "^1.5.1", + "screenfull": "^5.1.0", + "set-harmonic-interval": "^1.0.1", + "throttle-debounce": "^3.0.1", + "ts-easing": "^0.2.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -4036,6 +4167,11 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -4109,6 +4245,14 @@ "fsevents": "~2.3.2" } }, + "node_modules/rtl-css-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4140,6 +4284,17 @@ "loose-envify": "^1.1.0" } }, + "node_modules/screenfull": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", + "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -4157,6 +4312,14 @@ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" }, + "node_modules/set-harmonic-interval": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz", + "integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==", + "engines": { + "node": ">=6.9" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4200,6 +4363,14 @@ "tslib": "^2.0.3" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4209,6 +4380,46 @@ "node": ">=0.10.0" } }, + "node_modules/stack-generator": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", + "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" + }, + "node_modules/stacktrace-gps": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", + "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", + "dependencies": { + "source-map": "0.5.6", + "stackframe": "^1.3.4" + } + }, + "node_modules/stacktrace-gps/node_modules/source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stacktrace-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", + "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "dependencies": { + "error-stack-parser": "^2.0.6", + "stack-generator": "^2.0.5", + "stacktrace-gps": "^3.0.4" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -4317,6 +4528,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stylis": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.4.tgz", + "integrity": "sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -4427,6 +4643,14 @@ "node": ">=0.8" } }, + "node_modules/throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==", + "engines": { + "node": ">=10" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4439,6 +4663,11 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, "node_modules/ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", @@ -4451,6 +4680,11 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-easing": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz", + "integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==" + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -4480,8 +4714,7 @@ "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/turbo-stream": { "version": "2.4.0", diff --git a/package.json b/package.json index d26dd43..ce4b462 100755 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "react-leaflet": "^4.2.1", "react-router": "^7.0.2", "react-router-dom": "^7.0.2", - "react-transition-group": "^4.4.5" + "react-transition-group": "^4.4.5", + "react-use": "^17.5.1" }, "devDependencies": { "@eslint/js": "^9.15.0", diff --git a/src/_app/Providers/router/index.tsx b/src/_app/Providers/router/index.tsx index ef0ba09..95b19cb 100755 --- a/src/_app/Providers/router/index.tsx +++ b/src/_app/Providers/router/index.tsx @@ -19,7 +19,7 @@ const router = createBrowserRouter([ element: , }, { - path: "/capsule/message/create", + path: "/capsule/:code/message/create", element: , }, ]); diff --git a/src/_app/pages/CapsuleDetailPage/index.tsx b/src/_app/pages/CapsuleDetailPage/index.tsx index 5427bc1..62f903d 100644 --- a/src/_app/pages/CapsuleDetailPage/index.tsx +++ b/src/_app/pages/CapsuleDetailPage/index.tsx @@ -1,9 +1,13 @@ import { useLoadingOverlay } from "@/_app/Providers/loadingOverlay"; import CustomButtons from "@/components/CustomButtons"; -import { useNavigate } from "react-router"; +import { isUndefined } from "@/utils"; +import { useMemo } from "react"; +import { useNavigate, useParams } from "react-router"; // 진입점 설정을 위해 임시로 작성되었습니다. const CapsuleDetailPage = () => { + const { code } = useParams(); + const capsuleCode = useMemo(() => (isUndefined(code) ? "" : code), [code]); const navigate = useNavigate(); const { setGlobalLoading } = useLoadingOverlay(); @@ -16,7 +20,9 @@ const CapsuleDetailPage = () => { > navigate("/capsule")} + onClick={() => + navigate(`/capsule/${encodeURIComponent(capsuleCode)}/message/create`) + } /> > ); diff --git a/src/_app/pages/CreateCapsulePage/Steps/NameInputStep/index.tsx b/src/_app/pages/CreateCapsulePage/Steps/NameInputStep/index.tsx index b9a2089..ca6d7aa 100644 --- a/src/_app/pages/CreateCapsulePage/Steps/NameInputStep/index.tsx +++ b/src/_app/pages/CreateCapsulePage/Steps/NameInputStep/index.tsx @@ -1,6 +1,5 @@ import CustomInput from "@/components/CustomInput"; import StepHeader from "@/components/Funnel/StepHeader"; -import Margin from "@/components/Margin"; interface Props { name: string; @@ -9,9 +8,9 @@ interface Props { const NameInputStep = ({ name, setName }: Props) => { return ( - + - + { return ( - + - + { // 양쪽 끝 인덱스 콜백 함수 const firstBackCallback = () => navigate("/"); - const lastNextCallback = async () => setIsCreateConfirmModalOpen(true); + const lastNextCallback = () => setIsCreateConfirmModalOpen(true); const [createdCapsuleCode, setCreatedCapsuleCode] = useState(); // 생성 확인 모달 관련 @@ -115,7 +116,7 @@ const CreateCapsulePage = () => { return ( <> - void; +} +const NameInputStep = ({ name, setName }: Props) => { + return ( + + + + + + + ); +}; + +export default NameInputStep; diff --git a/src/_app/pages/CreateMessagePage/Steps/NameInputStep/useNameInputStep.ts b/src/_app/pages/CreateMessagePage/Steps/NameInputStep/useNameInputStep.ts new file mode 100644 index 0000000..b6484bc --- /dev/null +++ b/src/_app/pages/CreateMessagePage/Steps/NameInputStep/useNameInputStep.ts @@ -0,0 +1,30 @@ +import { getInputCapsuleNameValid } from "@/utils/validations"; +import { useEffect, useMemo, useState } from "react"; + +const useNameInputStep = () => { + const [inputName, setInputName] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + const buttonDisabled = useMemo(() => inputName.length === 0, [inputName]); + + const onClickButton = () => { + const isValid = getInputCapsuleNameValid(inputName); + + if (!isValid) setErrorMessage("캡슐 이름이 유효하지 않습니다."); + + return isValid; + }; + + const stepProps = { + BottomButton: { + onClick: onClickButton, + disabled: buttonDisabled, + }, + errorMessage: errorMessage, + }; + + useEffect(() => setErrorMessage(""), [inputName]); + + return { inputName, setInputName, stepProps }; +}; + +export default useNameInputStep; diff --git a/src/_app/pages/CreateMessagePage/Steps/PhotoInputStep/index.tsx b/src/_app/pages/CreateMessagePage/Steps/PhotoInputStep/index.tsx new file mode 100644 index 0000000..6e53895 --- /dev/null +++ b/src/_app/pages/CreateMessagePage/Steps/PhotoInputStep/index.tsx @@ -0,0 +1,101 @@ +import { useLoadingOverlay } from "@/_app/Providers/loadingOverlay"; +import IconPlus from "@/assets/icons/plus-icon.svg?react"; +import StepHeader from "@/components/Funnel/StepHeader"; +import { Photo } from "@/types/client"; +import { isNill, isUndefined } from "@/utils"; +import clsx from "clsx"; +import { ChangeEvent, useRef } from "react"; +import { useWindowSize } from "react-use"; + +interface Props { + photo: Photo | undefined; + setPhoto: (newPhoto: Photo) => void; +} +const PhotoInputStep = ({ photo, setPhoto }: Props) => { + const { setGlobalLoading } = useLoadingOverlay(); + const inputRef = useRef(null); + const onClickUpload = () => { + if (isNill(inputRef.current)) { + return; + } + + inputRef.current.click(); + }; + const onChange = async (event: ChangeEvent) => { + if (isNill(event.target) || isNill(event.target.files)) { + return; + } + + const file = event.target.files[0]; + setGlobalLoading(true); + + const maxSize = 5 * 1024 * 1024; // 10MB + if (file.size > maxSize) { + setGlobalLoading(false); + + return; + } + + const fileUrl = URL.createObjectURL(file); + + const img = new window.Image(); + img.src = fileUrl; + img.onload = () => + setPhoto({ + file, + url: fileUrl, + }); + setGlobalLoading(false); + }; + + const { height } = useWindowSize(); + + return ( + <> + + + + + + {isUndefined(photo) ? ( + + + + + 0 + {` / 1`} + + + + ) : ( + + )} + + + + 사진은 최대 1장, 5mb까지 업로드 가능해요. + + + + > + ); +}; + +export default PhotoInputStep; diff --git a/src/_app/pages/CreateMessagePage/Steps/PhotoInputStep/usePhotoInputStep.ts b/src/_app/pages/CreateMessagePage/Steps/PhotoInputStep/usePhotoInputStep.ts new file mode 100644 index 0000000..1dab535 --- /dev/null +++ b/src/_app/pages/CreateMessagePage/Steps/PhotoInputStep/usePhotoInputStep.ts @@ -0,0 +1,26 @@ +import { Photo } from "@/types/client"; +import { useEffect, useMemo, useState } from "react"; + +const usePhotoInputStep = () => { + const [inputPhoto, setInputPhoto] = useState(); + const [errorMessage, setErrorMessage] = useState(""); + const buttonDisabled = useMemo(() => false, []); + + const onClickButton = () => { + return true; + }; + + const stepProps = { + BottomButton: { + onClick: onClickButton, + disabled: buttonDisabled, + }, + errorMessage: errorMessage, + }; + + useEffect(() => setErrorMessage(""), [inputPhoto]); + + return { inputPhoto, setInputPhoto, stepProps }; +}; + +export default usePhotoInputStep; diff --git a/src/_app/pages/CreateMessagePage/Steps/TextInputStep/index.tsx b/src/_app/pages/CreateMessagePage/Steps/TextInputStep/index.tsx new file mode 100644 index 0000000..01ca545 --- /dev/null +++ b/src/_app/pages/CreateMessagePage/Steps/TextInputStep/index.tsx @@ -0,0 +1,24 @@ +import CustomTextArea from "@/components/CustomTextArea"; +import StepHeader from "@/components/Funnel/StepHeader"; + +interface Props { + text: string; + setText: (newText: string) => void; +} +const TextInputStep = ({ text, setText }: Props) => { + return ( + + + + + + + ); +}; + +export default TextInputStep; diff --git a/src/_app/pages/CreateMessagePage/Steps/TextInputStep/useTextInputStep.ts b/src/_app/pages/CreateMessagePage/Steps/TextInputStep/useTextInputStep.ts new file mode 100644 index 0000000..054f0a2 --- /dev/null +++ b/src/_app/pages/CreateMessagePage/Steps/TextInputStep/useTextInputStep.ts @@ -0,0 +1,30 @@ +import { getInputCapsuleTextValid } from "@/utils/validations"; +import { useEffect, useMemo, useState } from "react"; + +const useTextInputStep = () => { + const [inputText, setInputText] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + const buttonDisabled = useMemo(() => inputText.length === 0, [inputText]); + + const onClickButton = () => { + const isValid = getInputCapsuleTextValid(inputText); + + if (!isValid) setErrorMessage("입력이 유효하지 않습니다."); + + return isValid; + }; + + const stepProps = { + BottomButton: { + onClick: onClickButton, + disabled: buttonDisabled, + }, + errorMessage: errorMessage, + }; + + useEffect(() => setErrorMessage(""), [inputText]); + + return { inputText, setInputText, stepProps }; +}; + +export default useTextInputStep; diff --git a/src/_app/pages/CreateMessagePage/index.tsx b/src/_app/pages/CreateMessagePage/index.tsx index 264ebe2..89840db 100644 --- a/src/_app/pages/CreateMessagePage/index.tsx +++ b/src/_app/pages/CreateMessagePage/index.tsx @@ -1,5 +1,122 @@ +import { useLoadingOverlay } from "@/_app/Providers/loadingOverlay"; +import MessageCreateFunnel from "@/components/Funnel/MessageCreateFunnel"; +import MessageCreateCompleteModal from "@/components/Modals/MessageCreateCompleteModal"; +import MessageCreateConfirmModal from "@/components/Modals/MessageCreateConfirmModal"; +import { useMessageMutate } from "@/queries/Message/useMessageService"; +import { Step } from "@/types/client"; +import { isUndefined } from "@/utils"; +import { useMemo, useState } from "react"; +import { useNavigate, useParams } from "react-router"; +import NameInputStep from "./Steps/NameInputStep"; +import useNameInputStep from "./Steps/NameInputStep/useNameInputStep"; +import PhotoInputStep from "./Steps/PhotoInputStep"; +import usePhotoInputStep from "./Steps/PhotoInputStep/usePhotoInputStep"; +import TextInputStep from "./Steps/TextInputStep"; +import useTextInputStep from "./Steps/TextInputStep/useTextInputStep"; + const CreateMessagePage = () => { - return <>CreateMessagePage>; + const { code } = useParams(); + const capsuleCode = useMemo(() => (isUndefined(code) ? "" : code), [code]); + const navigate = useNavigate(); + + const { + inputName, + setInputName, + stepProps: nameInputStepProps, + } = useNameInputStep(); + + const { + inputText, + setInputText, + stepProps: textInputStepProps, + } = useTextInputStep(); + + const { + inputPhoto, + setInputPhoto, + stepProps: photoInputStepProps, + } = usePhotoInputStep(); + + const steps: Array = [ + { + children: , + ...nameInputStepProps, + }, + { + children: , + ...textInputStepProps, + }, + { + children: , + ...photoInputStepProps, + }, + ]; + + // 양쪽 끝 인덱스 콜백 함수 + const firstBackCallback = () => navigate(-1); + const lastNextCallback = () => setIsCreateConfirmModalOpen(true); + + // 생성 확인 모달 관련 + const [isCreateConfirmModalOpen, setIsCreateConfirmModalOpen] = + useState(false); + const hideCreateConfirmModal = () => setIsCreateConfirmModalOpen(false); + const { mutateAsync } = useMessageMutate({ code: capsuleCode }); + const { setGlobalLoading } = useLoadingOverlay(); + + const createConfirmModalCallback = async () => { + setGlobalLoading(true); + + const formData = new FormData(); + formData.append( + "dto", + JSON.stringify({ + useName: inputName, + text: inputText, + code: capsuleCode, + }) + ); + if (!isUndefined(inputPhoto)) formData.append("file", inputPhoto?.file); + + mutateAsync(formData) + .then(() => { + setTimeout(() => setIsCreateCompleteModalOpen(true), 1000); + }) + .finally(() => { + setIsCreateConfirmModalOpen(false); + setTimeout(() => setGlobalLoading(false), 1000); + }); + }; + + // 생성 완료 모달 관련 + const [isCreateCompleteModalOpen, setIsCreateCompleteModalOpen] = + useState(false); + const hideCreateCompleteModal = () => setIsCreateCompleteModalOpen(false); + const createCompleteModalCallback = async () => { + hideCreateCompleteModal(); + navigate(`/capsule/${encodeURIComponent(capsuleCode)}`); + }; + + return ( + + + {isCreateConfirmModalOpen && ( + + )} + {isCreateCompleteModalOpen && ( + + )} + + ); }; export default CreateMessagePage; diff --git a/src/assets/css/reset.css b/src/assets/css/reset.css index 82935ae..b5e4cfe 100644 --- a/src/assets/css/reset.css +++ b/src/assets/css/reset.css @@ -122,3 +122,6 @@ table { border-collapse: collapse; border-spacing: 0; } +textarea { + resize: none; +} diff --git a/src/assets/icons/plus-icon.svg b/src/assets/icons/plus-icon.svg new file mode 100644 index 0000000..7c122c3 --- /dev/null +++ b/src/assets/icons/plus-icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/components/CustomTextArea/index.tsx b/src/components/CustomTextArea/index.tsx new file mode 100644 index 0000000..bd59b98 --- /dev/null +++ b/src/components/CustomTextArea/index.tsx @@ -0,0 +1,50 @@ +import { isNull } from "@/utils"; +import { useEffect, useRef } from "react"; +import { useWindowSize } from "react-use"; + +interface Props { + label?: string; + value: string; + setValue: (value: string) => void; + placeholder?: string; + mountFocus?: boolean; +} +const CustomTextArea = ({ + label, + value, + setValue, + placeholder, + mountFocus = false, +}: Props) => { + const inputRef = useRef(null); + const handleChange: React.ChangeEventHandler = (event) => + setValue(event.target.value); + + useEffect(() => { + if (isNull(inputRef.current) || !mountFocus) return; + + inputRef.current.focus(); + }, []); + + const { height } = useWindowSize(); + + return ( + + {label && {label}} + + + ); +}; + +export default CustomTextArea; diff --git a/src/components/Funnel/index.tsx b/src/components/Funnel/CapsuleCreateFunnel/index.tsx similarity index 85% rename from src/components/Funnel/index.tsx rename to src/components/Funnel/CapsuleCreateFunnel/index.tsx index 4e78851..0ac0245 100644 --- a/src/components/Funnel/index.tsx +++ b/src/components/Funnel/CapsuleCreateFunnel/index.tsx @@ -1,25 +1,20 @@ import BackButtonHeader from "@/components/BackButtonHeader"; import CustomButtons from "@/components/CustomButtons"; import StepProgress from "@/components/Progress/StepProgress"; +import { Step } from "@/types/client"; import { isUndefined } from "@/utils"; import { useMemo, useState } from "react"; -export interface Step { - children: JSX.Element; - BottomButton: { - title?: string; - onClick: () => boolean; - disabled?: boolean; - }; - errorMessage: string; -} - interface Props { steps: Array; firstBackCallback: () => void; lastNextCallback: () => void; } -const Funnel = ({ steps, firstBackCallback, lastNextCallback }: Props) => { +const CapsuleCreateFunnel = ({ + steps, + firstBackCallback, + lastNextCallback, +}: Props) => { const [focusedStepIndex, setFocusedStepIndex] = useState(0); const focusedStep = useMemo( @@ -75,4 +70,4 @@ const Funnel = ({ steps, firstBackCallback, lastNextCallback }: Props) => { ); }; -export default Funnel; +export default CapsuleCreateFunnel; diff --git a/src/components/Funnel/MessageCreateFunnel/index.tsx b/src/components/Funnel/MessageCreateFunnel/index.tsx new file mode 100644 index 0000000..70d43fe --- /dev/null +++ b/src/components/Funnel/MessageCreateFunnel/index.tsx @@ -0,0 +1,74 @@ +import BackButtonHeader from "@/components/BackButtonHeader"; +import CustomButtons from "@/components/CustomButtons"; +import CapsuleProgress from "@/components/Progress/CapsuleProgress"; +import { Step } from "@/types/client"; +import { isUndefined } from "@/utils"; +import { useMemo, useState } from "react"; + +interface Props { + steps: Array; + firstBackCallback: () => void; + lastNextCallback: () => void; +} +const MessageCreateFunnel = ({ + steps, + firstBackCallback, + lastNextCallback, +}: Props) => { + const [focusedStepIndex, setFocusedStepIndex] = useState(0); + + const focusedStep = useMemo( + () => + focusedStepIndex <= steps.length - 1 + ? steps[focusedStepIndex] + : undefined, + [steps, focusedStepIndex] + ); + + const goBack = () => { + if (isUndefined(focusedStep)) return; + + if (focusedStepIndex <= 0) { + firstBackCallback(); + + return; + } + + const isValid = focusedStep.BottomButton.onClick(); + + if (isValid) setFocusedStepIndex((prev) => prev - 1); + }; + + const goNext = () => { + if (isUndefined(focusedStep)) return; + + const isValid = focusedStep.BottomButton.onClick(); + + if (!isValid) return; + + if (focusedStepIndex >= steps.length - 1) { + lastNextCallback(); + + return; + } + + setFocusedStepIndex((prev) => prev + 1); + }; + + return ( + + + + + {!isUndefined(focusedStep) && focusedStep.children} + + + ); +}; + +export default MessageCreateFunnel; diff --git a/src/components/Modals/MessageCreateCompleteModal/index.tsx b/src/components/Modals/MessageCreateCompleteModal/index.tsx new file mode 100644 index 0000000..39ead61 --- /dev/null +++ b/src/components/Modals/MessageCreateCompleteModal/index.tsx @@ -0,0 +1,19 @@ +import IconComplete from "@/assets/icons/complete-icon.svg?react"; +import OneButtonModal from "../ModalBases/OneButtonModal"; + +interface Props { + hideModal: () => void; + onClick: () => void; +} +// TODO: mark up +const MessageCreateCompleteModal = ({ hideModal, onClick }: Props) => ( + + + + 캡슐이 추가되었습니다! + 야호! + + +); + +export default MessageCreateCompleteModal; diff --git a/src/components/Modals/MessageCreateConfirmModal/index.tsx b/src/components/Modals/MessageCreateConfirmModal/index.tsx new file mode 100644 index 0000000..06d29f9 --- /dev/null +++ b/src/components/Modals/MessageCreateConfirmModal/index.tsx @@ -0,0 +1,17 @@ +import TwoButtonModal from "../ModalBases/TwoButtonModal"; + +interface Props { + hideModal: () => void; + confirmCallback: () => void; +} +const MessageCreateConfirmModal = ({ hideModal, confirmCallback }: Props) => ( + + {"캡슐을 추가하시겠습니까?"} + +); + +export default MessageCreateConfirmModal; diff --git a/src/components/Progress/CapsuleProgress/index.tsx b/src/components/Progress/CapsuleProgress/index.tsx new file mode 100644 index 0000000..5f69ae8 --- /dev/null +++ b/src/components/Progress/CapsuleProgress/index.tsx @@ -0,0 +1,27 @@ +import clsx from "clsx"; + +interface Props { + max: number; + current: number; +} +const CapsuleProgress = ({ max, current }: Props) => { + return ( + + + + {`알맹이 ${current + 1} / `} + {`${max}${ + current === 2 ? ` (선택)` : "" + }`} + + + + ); +}; + +export default CapsuleProgress; diff --git a/src/queries/Capsule/CapsuleService.ts b/src/queries/Capsule/CapsuleService.ts index 79a9b34..b8c4dde 100644 --- a/src/queries/Capsule/CapsuleService.ts +++ b/src/queries/Capsule/CapsuleService.ts @@ -1,4 +1,4 @@ -import { Capsule } from "@/types/server"; +import { Capsule, responseType } from "@/types/server"; import getQueryString from "@/utils/getQueryString"; import Service from "../Service"; @@ -11,13 +11,7 @@ class CapsuleService extends Service { goalTime: number; capsuleDesignId: number; password: string; - }) => - this.http.post<{ - success: boolean; - code: number; - message: string; - data: string; - }>(`/capsule`, body); + }) => this.http.post>(`/capsule`, body); } export default new CapsuleService(); diff --git a/src/queries/Capsule/useCapsuleService.ts b/src/queries/Capsule/useCapsuleService.ts index aa645d5..4c4df65 100644 --- a/src/queries/Capsule/useCapsuleService.ts +++ b/src/queries/Capsule/useCapsuleService.ts @@ -1,7 +1,7 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import CapsuleService from "./CapsuleService"; -const queryKeys = { +export const queryKeys = { capsule: ({ code }: { code: string }) => ["capsule", code] as const, }; diff --git a/src/queries/Message/MessageService.ts b/src/queries/Message/MessageService.ts new file mode 100644 index 0000000..61782de --- /dev/null +++ b/src/queries/Message/MessageService.ts @@ -0,0 +1,13 @@ +import { responseType } from "@/types/server"; +import Service from "../Service"; + +class MessageService extends Service { + postMessage = (formData: FormData) => + this.http.post>(``, formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }); +} + +export default new MessageService(); diff --git a/src/queries/Message/useMessageService.ts b/src/queries/Message/useMessageService.ts new file mode 100644 index 0000000..806fcd9 --- /dev/null +++ b/src/queries/Message/useMessageService.ts @@ -0,0 +1,21 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { queryKeys as CapsuleQueryKeys } from "../Capsule/useCapsuleService"; +import MessageService from "./MessageService"; + +interface useMessageMutateProps { + code: string; +} +export const useMessageMutate = ({ code }: useMessageMutateProps) => { + const queryClient = useQueryClient(); + + return useMutation( + { + mutationFn: (formData: FormData) => MessageService.postMessage(formData), + onSuccess: () => + queryClient.invalidateQueries({ + queryKey: CapsuleQueryKeys.capsule({ code }), + }), + }, + queryClient + ); +}; diff --git a/src/types/client.ts b/src/types/client.ts index 6197251..678c72b 100644 --- a/src/types/client.ts +++ b/src/types/client.ts @@ -5,3 +5,18 @@ export type Browser = | "InAppFacebook" | "InAppTicTok" | "InAppLine"; + +export interface Step { + children: JSX.Element; + BottomButton: { + title?: string; + onClick: () => boolean; + disabled?: boolean; + }; + errorMessage: string; +} + +export interface Photo { + file: File; + url: string; +} diff --git a/src/types/server.ts b/src/types/server.ts index 0658212..af012da 100644 --- a/src/types/server.ts +++ b/src/types/server.ts @@ -1,3 +1,10 @@ +export interface responseType { + success: boolean; + code: number; + message: string; + data: T; +} + /** * Message */ diff --git a/src/utils/validations.ts b/src/utils/validations.ts index 552957c..83b503b 100644 --- a/src/utils/validations.ts +++ b/src/utils/validations.ts @@ -6,4 +6,7 @@ export const getInputCapsuleNameValid = (name: string) => typeof name === "string"; export const getInputCapsulePasswordValid = (password: string) => - typeof password === "string"; \ No newline at end of file + typeof password === "string"; +export const getInputCapsuleTextValid = (text: string) => + typeof text === "string"; + diff --git a/tailwind.config.js b/tailwind.config.js index edf23cd..b8f185b 100755 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -7,12 +7,13 @@ export default { "primary-main": "#5194F9", "primary-text": "#FFFFFF", "primary-disabled": "#A1A1A1", - "primary-disabled-text": "#000", + "primary-disabled-text": "#FFFFFF", "primary-paper": "#F6FAFF", "secondary-main": "#DFE3EA", "secondary-text": "#525252", + "border-grey": "#DEDEDE", error: "#FF3321", }, },
+ 0 + {` / 1`} +
+ 사진은 최대 1장, 5mb까지 업로드 가능해요. +
캡슐이 추가되었습니다!
야호!
{"캡슐을 추가하시겠습니까?"}
+ {`알맹이 ${current + 1} / `} + {`${max}${ + current === 2 ? ` (선택)` : "" + }`} +