From 8808f239cee2a203c821cfa6383735b3fd4a6b3b Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Wed, 13 Nov 2024 16:49:15 +0000 Subject: [PATCH 01/19] add slider --- app/package-lock.json | 203 +++++++++++++++++++++++++++++++++++------- app/package.json | 4 +- 2 files changed, 174 insertions(+), 33 deletions(-) diff --git a/app/package-lock.json b/app/package-lock.json index cff4bb8..65447df 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -16,6 +16,7 @@ "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-select": "^2.1.2", + "@radix-ui/react-slider": "^1.2.1", "@radix-ui/react-slot": "^1.1.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -23,6 +24,7 @@ "firebase": "^11.0.1", "firebase-admin": "^12.7.0", "formidable": "^3.5.2", + "jotai": "^2.10.2", "lucide-react": "^0.453.0", "minio": "^8.0.2", "next": "14.2.16", @@ -1753,12 +1755,12 @@ "@biomejs/cli-win32-x64": "1.9.4" } }, - "node_modules/@biomejs/cli-linux-arm64": { + "node_modules/@biomejs/cli-linux-x64": { "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", - "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", + "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT OR Apache-2.0", @@ -1770,12 +1772,12 @@ "node": ">=14.21.3" } }, - "node_modules/@biomejs/cli-linux-arm64-musl": { + "node_modules/@biomejs/cli-linux-x64-musl": { "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", - "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", + "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT OR Apache-2.0", @@ -2458,7 +2460,6 @@ "version": "1.6.8", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", - "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.8" } @@ -2467,7 +2468,6 @@ "version": "1.6.12", "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", - "license": "MIT", "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/utils": "^0.2.8" @@ -2477,7 +2477,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", - "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.0.0" }, @@ -2489,8 +2488,7 @@ "node_modules/@floating-ui/utils": { "version": "0.2.8", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", - "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==", - "license": "MIT" + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" }, "node_modules/@google-cloud/firestore": { "version": "7.10.0", @@ -2702,6 +2700,36 @@ "integrity": "sha512-fLrX5TfJzHCbnZ9YUSnGW63tMV3L4nSfhgOQ0iCcX21Pt+VSTDuaLsSuL8J/2XAiVA5AnzvXDpf6pMs60QxOag==", "license": "MIT" }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.16", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.16.tgz", + "integrity": "sha512-uFT34QojYkf0+nn6MEZ4gIWQ5aqGF11uIZ1HSxG+cSbj+Mg3+tYm8qXYd3dKN5jqKUm5rBVvf1PBRO/MeQ6rxw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.16", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.16.tgz", + "integrity": "sha512-mCecsFkYezem0QiZlg2bau3Xul77VxUD38b/auAjohMA22G9KTJneUYMv78vWoCCFkleFAhY1NIvbyjj1ncG9g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@next/swc-linux-arm64-gnu": { "version": "14.2.16", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.16.tgz", @@ -2709,7 +2737,6 @@ "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -2725,6 +2752,37 @@ "cpu": [ "arm64" ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.16", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.16.tgz", + "integrity": "sha512-9AGcX7VAkGbc5zTSa+bjQ757tkjr6C/pKS7OK8cX7QEiK6MHIIezBLcQ7gQqbDW2k5yaqba2aDtaBeyyZh1i6Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.16", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.16.tgz", + "integrity": "sha512-Klgeagrdun4WWDaOizdbtIIm8khUDQJ/5cRzdpXHfkbY91LxBXeejL4kbZBrpR/nmgRrQvmz4l3OtttNVkz2Sg==", + "cpu": [ + "x64" + ], "license": "MIT", "optional": true, "os": [ @@ -2734,6 +2792,51 @@ "node": ">= 10" } }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.16", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.16.tgz", + "integrity": "sha512-PwW8A1UC1Y0xIm83G3yFGPiOBftJK4zukTmk7DI1CebyMOoaVpd8aSy7K6GhobzhkjYvqS/QmzcfsWG2Dwizdg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.16", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.16.tgz", + "integrity": "sha512-jhPl3nN0oKEshJBNDAo0etGMzv0j3q3VYorTSFqH1o3rwv1MQRdor27u1zhkgsHPNeY1jxcgyx1ZsCkDD1IHgg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.16", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.16.tgz", + "integrity": "sha512-OA7NtfxgirCjfqt+02BqxC3MIgM/JaGjw9tOe4fyZgPsqfseNiMPnCRP44Pfs+Gpo9zPN+SXaFsgP6vk8d571A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2924,8 +3027,7 @@ "node_modules/@radix-ui/number": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", - "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==", - "license": "MIT" + "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==" }, "node_modules/@radix-ui/primitive": { "version": "1.1.0", @@ -2965,7 +3067,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", "integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==", - "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.0.0" }, @@ -2988,7 +3089,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==", - "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-context": "1.1.0", @@ -3014,7 +3114,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", - "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -3059,7 +3158,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz", "integrity": "sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==", - "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", @@ -3095,7 +3193,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", - "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -3227,7 +3324,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", "integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==", - "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.0", @@ -3259,7 +3355,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", - "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -3345,7 +3440,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.0.tgz", "integrity": "sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==", - "license": "MIT", "dependencies": { "@radix-ui/react-context": "1.1.0", "@radix-ui/react-primitive": "2.0.0" @@ -3369,7 +3463,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", - "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -3384,7 +3477,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.2.tgz", "integrity": "sha512-rZJtWmorC7dFRi0owDmoijm6nSJH1tVw64QGiNIZ9PNLyBDtG+iAq+XGsya052At4BfarzY/Dhv9wrrUr6IMZA==", - "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.0", @@ -3423,6 +3515,38 @@ } } }, + "node_modules/@radix-ui/react-slider": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.1.tgz", + "integrity": "sha512-bEzQoDW0XP+h/oGbutF5VMWJPAl/UU8IJjr7h02SOHDIIIxq+cep8nItVNoBV+OMmahCdqdF38FTpmXoqQUGvw==", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.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-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", @@ -3511,7 +3635,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", - "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -3526,7 +3649,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", - "license": "MIT", "dependencies": { "@radix-ui/rect": "1.1.0" }, @@ -3544,7 +3666,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", - "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, @@ -3562,7 +3683,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz", "integrity": "sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==", - "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.0.0" }, @@ -3584,8 +3704,7 @@ "node_modules/@radix-ui/rect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", - "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", - "license": "MIT" + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", @@ -7687,6 +7806,26 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/jotai": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.10.2.tgz", + "integrity": "sha512-DqsBTlRglIBviuJLfK6JxZzpd6vKfbuJ4IqRCz70RFEDeZf46Fcteb/FXxNr1UnoxR5oUy3oq7IE8BrEq0G5DQ==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=17.0.0", + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/app/package.json b/app/package.json index e7f3962..d482bab 100755 --- a/app/package.json +++ b/app/package.json @@ -18,8 +18,9 @@ "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", - "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-progress": "^1.1.0", + "@radix-ui/react-select": "^2.1.2", + "@radix-ui/react-slider": "^1.2.1", "@radix-ui/react-slot": "^1.1.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -27,6 +28,7 @@ "firebase": "^11.0.1", "firebase-admin": "^12.7.0", "formidable": "^3.5.2", + "jotai": "^2.10.2", "lucide-react": "^0.453.0", "minio": "^8.0.2", "next": "14.2.16", From a9533051513823d5a92aa93c49dcff38bbfa711f Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Wed, 13 Nov 2024 16:49:26 +0000 Subject: [PATCH 02/19] =?UTF-8?q?atom=E3=81=AE=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/lib/atom.ts | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 app/src/lib/atom.ts diff --git a/app/src/lib/atom.ts b/app/src/lib/atom.ts new file mode 100644 index 0000000..50c06c6 --- /dev/null +++ b/app/src/lib/atom.ts @@ -0,0 +1,8 @@ +import { atom } from "jotai"; + +export const statusChangeDialogOpenAtom = atom(false); + +export const openStatusChangeDialogOpenAtom = atom( + (get) => get(statusChangeDialogOpenAtom), + (get, set) => set(statusChangeDialogOpenAtom, true), +); From 14b466a227d29df5dfb62497ac7e47c67ce92b24 Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Wed, 13 Nov 2024 16:49:52 +0000 Subject: [PATCH 03/19] =?UTF-8?q?=E3=82=B9=E3=83=86=E3=83=BC=E3=82=BF?= =?UTF-8?q?=E3=82=B9=E8=AA=BF=E6=95=B4=E3=83=80=E3=82=A4=E3=82=A2=E3=83=AD?= =?UTF-8?q?=E3=82=B0=E3=81=AE=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../view/user/StatusChangeDialog.tsx | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 app/src/components/view/user/StatusChangeDialog.tsx diff --git a/app/src/components/view/user/StatusChangeDialog.tsx b/app/src/components/view/user/StatusChangeDialog.tsx new file mode 100644 index 0000000..6664355 --- /dev/null +++ b/app/src/components/view/user/StatusChangeDialog.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Slider } from "@/components/ui/slider"; +import { useState } from "react"; + +interface PlayStyleSettings { + speed: number; + similarity: number; + total: number; +} + +export default function StatusChangeDialog() { + const [open, setOpen] = useState(true); + const [settings, setSettings] = useState({ + speed: 12, + similarity: 70, + total: 100, + }); + + // TODO APIの繋ぎ込みをおこなう。その際に値のバリデージョンも実装する。 + const handleSave = async () => { + setOpen(false); + }; + + const remainingPoints = + settings.total - (settings.speed + settings.similarity); + + return ( + + + + + プレイスタイルの調整 + + +
+

+ 指定したポイントを速度と正確性に振り分けてプレイスタイルをカスタマイズできます。 +

+
+
+
+ + + {settings.speed} / {settings.total} + +
+ + setSettings({ ...settings, speed: value }) + } + className="[&_[role=slider]]:h-4 [&_[role=slider]]:w-4" + /> +
+
+
+ + + {settings.similarity} / {settings.total} + +
+ + setSettings({ ...settings, similarity: value }) + } + className="[&_[role=slider]]:h-4 [&_[role=slider]]:w-4" + /> +
+
+ 未割り当てポイント: + {remainingPoints} +
+
+
+ + +
+
+
+
+ ); +} From cc519281a836226e60baa2b32adfd60dc0b6fb96 Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Wed, 13 Nov 2024 16:50:18 +0000 Subject: [PATCH 04/19] =?UTF-8?q?slider=E3=81=AE=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/components/ui/slider.tsx | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 app/src/components/ui/slider.tsx diff --git a/app/src/components/ui/slider.tsx b/app/src/components/ui/slider.tsx new file mode 100644 index 0000000..3113ead --- /dev/null +++ b/app/src/components/ui/slider.tsx @@ -0,0 +1,28 @@ +"use client"; + +import * as SliderPrimitive from "@radix-ui/react-slider"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + +)); +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider }; From 633baa0daa4be4370fb197034ff7dce7408a963a Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Wed, 13 Nov 2024 16:50:44 +0000 Subject: [PATCH 05/19] =?UTF-8?q?=E3=82=B9=E3=83=86=E3=83=BC=E3=82=BF?= =?UTF-8?q?=E3=82=B9=E8=AA=BF=E6=95=B4=E3=83=80=E3=82=A4=E3=82=A2=E3=83=AD?= =?UTF-8?q?=E3=82=B0=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=95=E3=81=9B=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/app/user/page.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/app/user/page.tsx b/app/src/app/user/page.tsx index 5c5f2ea..e98d7dd 100644 --- a/app/src/app/user/page.tsx +++ b/app/src/app/user/page.tsx @@ -3,7 +3,10 @@ import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; +import StatusChangeDialog from "@/components/view/user/StatusChangeDialog"; +import { openStatusChangeDialogOpenAtom } from "@/lib/atom"; import type { MyScoreDetail, User } from "@/types"; +import { useSetAtom } from "jotai"; import { useEffect, useState } from "react"; import { FiEdit2 } from "react-icons/fi"; import { LuClock, LuFlame, LuTrophy } from "react-icons/lu"; @@ -13,6 +16,7 @@ const UserPage = () => { const [userData, setUserData] = useState(); const [myScore, setMyScore] = useState([]); const [isEditing, setIsEditing] = useState(false); + const open = useSetAtom(openStatusChangeDialogOpenAtom); useEffect(() => { const fetchData = async () => { @@ -41,9 +45,14 @@ const UserPage = () => { fetchData(); }, []); + useEffect(() => { + open; + }); + if (!userData) return null; return (
+
{userData.photoURL ? ( Date: Thu, 14 Nov 2024 09:30:10 +0000 Subject: [PATCH 06/19] =?UTF-8?q?add=20=E3=82=B9=E3=83=86=E3=83=BC?= =?UTF-8?q?=E3=82=BF=E3=82=B9=E4=B8=80=E8=A6=A7=E3=82=B3=E3=83=B3=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88=E3=81=AE=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/components/view/user/StatusList.tsx | 55 +++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 app/src/components/view/user/StatusList.tsx diff --git a/app/src/components/view/user/StatusList.tsx b/app/src/components/view/user/StatusList.tsx new file mode 100644 index 0000000..792d8f8 --- /dev/null +++ b/app/src/components/view/user/StatusList.tsx @@ -0,0 +1,55 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { ChevronRight } from "lucide-react"; + +type StatusListProps = { + speedPoint: number; + similarityPoint: number; +}; + +export default function StatusList({ + speedPoint, + similarityPoint, +}: StatusListProps) { + return ( + + + + プレイスタイル設定 + + + + +

+ スピードと類似度のバランスを調整 +

+
+
+
+ スピード + {speedPoint}ポイント +
+ +
+ +
+
+
+ 類似度 + {similarityPoint}ポイント +
+ +
+ +
+
+ + + ); +} From 87ecf88cdf6f275cab39fa9ada1d4322c46eca8d Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Thu, 14 Nov 2024 09:30:52 +0000 Subject: [PATCH 07/19] =?UTF-8?q?=E3=82=B9=E3=83=86=E3=83=BC=E3=82=BF?= =?UTF-8?q?=E3=82=B9=E4=B8=80=E8=A6=A7=E3=81=8B=E3=82=89=E8=AA=BF=E6=95=B4?= =?UTF-8?q?=E3=83=80=E3=82=A4=E3=82=A2=E3=83=AD=E3=82=B0=E3=82=92=E9=96=8B?= =?UTF-8?q?=E3=81=91=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E7=B9=8B=E3=81=8E?= =?UTF-8?q?=E8=BE=BC=E3=81=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/app/user/page.tsx | 16 ++++++++-------- .../components/view/user/StatusChangeDialog.tsx | 6 ++++-- app/src/lib/atom.ts | 17 +++++++++++------ 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/app/src/app/user/page.tsx b/app/src/app/user/page.tsx index e98d7dd..2daca6a 100644 --- a/app/src/app/user/page.tsx +++ b/app/src/app/user/page.tsx @@ -4,9 +4,9 @@ import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import StatusChangeDialog from "@/components/view/user/StatusChangeDialog"; -import { openStatusChangeDialogOpenAtom } from "@/lib/atom"; +import StatusList from "@/components/view/user/StatusList"; +import { useStatusChangeDialog } from "@/lib/atom"; import type { MyScoreDetail, User } from "@/types"; -import { useSetAtom } from "jotai"; import { useEffect, useState } from "react"; import { FiEdit2 } from "react-icons/fi"; import { LuClock, LuFlame, LuTrophy } from "react-icons/lu"; @@ -16,7 +16,8 @@ const UserPage = () => { const [userData, setUserData] = useState(); const [myScore, setMyScore] = useState([]); const [isEditing, setIsEditing] = useState(false); - const open = useSetAtom(openStatusChangeDialogOpenAtom); + const [isOpen, setIsOpen] = useStatusChangeDialog(); + const handleOpenDialog = () => setIsOpen(true); useEffect(() => { const fetchData = async () => { @@ -45,14 +46,10 @@ const UserPage = () => { fetchData(); }, []); - useEffect(() => { - open; - }); - if (!userData) return null; return (
- + {isOpen && }
{userData.photoURL ? ( {

最高点

+

過去のチャレンジ

{myScore.length === 0 ? ( diff --git a/app/src/components/view/user/StatusChangeDialog.tsx b/app/src/components/view/user/StatusChangeDialog.tsx index 6664355..ea36222 100644 --- a/app/src/components/view/user/StatusChangeDialog.tsx +++ b/app/src/components/view/user/StatusChangeDialog.tsx @@ -8,6 +8,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Slider } from "@/components/ui/slider"; +import { useStatusChangeDialog } from "@/lib/atom"; import { useState } from "react"; interface PlayStyleSettings { @@ -23,10 +24,11 @@ export default function StatusChangeDialog() { similarity: 70, total: 100, }); + const [_, setIsOpen] = useStatusChangeDialog(); // TODO APIの繋ぎ込みをおこなう。その際に値のバリデージョンも実装する。 const handleSave = async () => { - setOpen(false); + setIsOpen(false); }; const remainingPoints = @@ -93,7 +95,7 @@ export default function StatusChangeDialog() {
- diff --git a/app/src/lib/atom.ts b/app/src/lib/atom.ts index 50c06c6..43e1916 100644 --- a/app/src/lib/atom.ts +++ b/app/src/lib/atom.ts @@ -1,8 +1,13 @@ -import { atom } from "jotai"; +import { atom, useAtom, useSetAtom } from "jotai"; -export const statusChangeDialogOpenAtom = atom(false); +export const statusChangeDialogAtom = atom(false); -export const openStatusChangeDialogOpenAtom = atom( - (get) => get(statusChangeDialogOpenAtom), - (get, set) => set(statusChangeDialogOpenAtom, true), -); +export const useStatusChangeDialog = () => useAtom(statusChangeDialogAtom); + +export const useOpenStatusChangeDialog = () => + useSetAtom(statusChangeDialogAtom); + +export const toggleStatusChangeDialog = () => { + const [isOpen, setIsOpen] = useStatusChangeDialog(); + setIsOpen(!isOpen); +}; From d7c6479ded15b2a0f559b753bed1bd5b96e52914 Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Thu, 14 Nov 2024 13:10:11 +0000 Subject: [PATCH 08/19] =?UTF-8?q?=E9=96=8B=E9=96=89=E3=81=AE=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/components/view/user/StatusChangeDialog.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/components/view/user/StatusChangeDialog.tsx b/app/src/components/view/user/StatusChangeDialog.tsx index ea36222..7775c72 100644 --- a/app/src/components/view/user/StatusChangeDialog.tsx +++ b/app/src/components/view/user/StatusChangeDialog.tsx @@ -18,13 +18,12 @@ interface PlayStyleSettings { } export default function StatusChangeDialog() { - const [open, setOpen] = useState(true); const [settings, setSettings] = useState({ speed: 12, similarity: 70, total: 100, }); - const [_, setIsOpen] = useStatusChangeDialog(); + const [isOpen, setIsOpen] = useStatusChangeDialog(); // TODO APIの繋ぎ込みをおこなう。その際に値のバリデージョンも実装する。 const handleSave = async () => { @@ -35,7 +34,7 @@ export default function StatusChangeDialog() { settings.total - (settings.speed + settings.similarity); return ( - + From 6540ff506606f3d8bd6208c55243fcdb3e0ebb93 Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Wed, 13 Nov 2024 15:51:08 +0000 Subject: [PATCH 09/19] add jotai and Dialog --- app/package-lock.json | 22 +++++++++++++++++++++- app/package.json | 1 + 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/app/package-lock.json b/app/package-lock.json index f9ecedd..1f2bd23 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -23,6 +23,7 @@ "firebase": "^11.0.1", "firebase-admin": "^12.7.0", "formidable": "^3.5.2", + "jotai": "^2.10.2", "lucide-react": "^0.453.0", "minio": "^8.0.2", "next": "14.2.16", @@ -3059,7 +3060,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz", "integrity": "sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==", - "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", @@ -7687,6 +7687,26 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/jotai": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.10.2.tgz", + "integrity": "sha512-DqsBTlRglIBviuJLfK6JxZzpd6vKfbuJ4IqRCz70RFEDeZf46Fcteb/FXxNr1UnoxR5oUy3oq7IE8BrEq0G5DQ==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=17.0.0", + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/app/package.json b/app/package.json index e7f3962..1ba7846 100755 --- a/app/package.json +++ b/app/package.json @@ -27,6 +27,7 @@ "firebase": "^11.0.1", "firebase-admin": "^12.7.0", "formidable": "^3.5.2", + "jotai": "^2.10.2", "lucide-react": "^0.453.0", "minio": "^8.0.2", "next": "14.2.16", From faf42440e4fb4c56ba59afd2eb6d3020d7a9d621 Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Wed, 13 Nov 2024 15:52:11 +0000 Subject: [PATCH 10/19] =?UTF-8?q?feat=20=E3=83=80=E3=82=A4=E3=82=A2?= =?UTF-8?q?=E3=83=AD=E3=82=B0=E3=81=AE=E9=96=8B=E9=96=89=E3=82=92jotai?= =?UTF-8?q?=E3=81=A7=E7=AE=A1=E7=90=86=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/lib/atom.ts | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 app/src/lib/atom.ts diff --git a/app/src/lib/atom.ts b/app/src/lib/atom.ts new file mode 100644 index 0000000..1f11231 --- /dev/null +++ b/app/src/lib/atom.ts @@ -0,0 +1,8 @@ +import { atom } from "jotai"; + +export const dialogOpenAtom = atom(false); + +export const openDialogAtom = atom( + (get) => get(dialogOpenAtom), + (get, set) => set(dialogOpenAtom, true), +); From 5713e21ce9793c05d37b04c81319c482d3670612 Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Wed, 13 Nov 2024 15:52:47 +0000 Subject: [PATCH 11/19] feat add Dialog component --- app/src/components/view/Dialog.tsx | 98 ++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 app/src/components/view/Dialog.tsx diff --git a/app/src/components/view/Dialog.tsx b/app/src/components/view/Dialog.tsx new file mode 100644 index 0000000..87fda01 --- /dev/null +++ b/app/src/components/view/Dialog.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { Calendar, Camera, Trophy, X } from "lucide-react"; +import type * as React from "react"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useAtom } from "jotai"; +import { dialogOpenAtom } from "../../lib/atom"; + +interface NotificationDialogProps { + type: "login" | "photo" | "ranking"; + customTitle?: string; + customMessage?: string; + customSubMessage?: string; +} + +export default function Component({ + type = "login", + customTitle, + customMessage, + customSubMessage, +}: NotificationDialogProps) { + const [isOpen, setIsOpen] = useAtom(dialogOpenAtom); + + const content = { + login: { + title: customTitle || "ログイン継続中!", + icon: Calendar, + iconColor: "text-blue-500", + message: customMessage || "1日目", + subMessage: customSubMessage || "ポイントを獲得しました!", + }, + photo: { + title: customTitle || "撮影に成功しました!", + icon: Camera, + iconColor: "text-green-500", + message: customMessage || "素晴らしい写真です!", + subMessage: customSubMessage || "ポイントを獲得しました!", + }, + ranking: { + title: customTitle || "ランキング結果発表!", + icon: Trophy, + iconColor: "text-yellow-500", + message: customMessage || "第10位", + subMessage: customSubMessage || "ポイントを獲得しました!", + }, + }; + + const { + title: dialogTitle, + icon: Icon, + iconColor, + message, + subMessage, + } = content[type]; + + return ( + + + + + + +
+

{dialogTitle}

+
+
+
+
+ +
+
+

{message}

+

{subMessage}

+
+ +
+
+
+ ); +} From a306b68776ff6e4db4124b1b34328e43c17e2e8d Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Wed, 13 Nov 2024 15:53:27 +0000 Subject: [PATCH 12/19] =?UTF-8?q?feat=20=E5=90=84=E3=83=9A=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E3=81=A7=E3=83=80=E3=82=A4=E3=82=A2=E3=83=AD=E3=82=B0?= =?UTF-8?q?=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=95=E3=81=9B=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/app/camera/page.tsx | 6 ++++++ app/src/app/page.tsx | 11 ++++++++++- app/src/app/ranking/page.tsx | 10 ++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/app/src/app/camera/page.tsx b/app/src/app/camera/page.tsx index e779b35..ad3e8ae 100644 --- a/app/src/app/camera/page.tsx +++ b/app/src/app/camera/page.tsx @@ -16,9 +16,11 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Toaster } from "@/components/ui/sonner"; +import { Dialog } from "@/components/view/Dialog"; import { shapeCaption } from "@/functions/shapeCaption"; import { postSimilarity } from "@/functions/simirality"; import type { todayAssignment } from "@/types"; +import { useSetAtom } from "jotai"; import type React from "react"; import { useEffect, useRef, useState } from "react"; import { Camera, type CameraType } from "react-camera-pro"; @@ -26,6 +28,7 @@ import { toast } from "sonner"; import AddImageIcon from "../../../public/icons/icon-add-image.svg"; import RotateCameraIcon from "../../../public/icons/icon-rotate-camera.svg"; import ShutterIcon from "../../../public/icons/icon-shutter.svg"; +import { openDialogAtom } from "../../lib/atom"; interface ImagePreviewProps { image: string | null; @@ -109,6 +112,7 @@ const CameraApp = () => { >(); const [assignments, setAssignments] = useState([]); const [isActive, setIsActive] = useState(true); + const openDialog = useSetAtom(openDialogAtom); useEffect(() => { const getDevices = async () => { @@ -330,6 +334,7 @@ const CameraApp = () => { if (newAssignments.every((assignment) => assignment.isAnswered)) { setIsActive(false); } + openDialog; } catch (error) { setIsUploading(false); console.error("アップロード中にエラーが発生しました:", error); @@ -356,6 +361,7 @@ const CameraApp = () => { <> {isActive ? ( <> +
{ const fetchData = async () => { @@ -68,8 +72,13 @@ export default function Home() { fetchData(); }, []); + useEffect(() => { + openDialog(); + }, [openDialog]); + return (
+
{isLoading ? ( diff --git a/app/src/app/ranking/page.tsx b/app/src/app/ranking/page.tsx index d538097..123b6aa 100644 --- a/app/src/app/ranking/page.tsx +++ b/app/src/app/ranking/page.tsx @@ -1,7 +1,9 @@ "use client"; import { Button } from "@/components/ui/button"; +import Dialog from "@/components/view/Dialog"; import { ImageList } from "@/components/view/ranking/ImageList"; import type { todayAssignment } from "@/types"; +import { useSetAtom } from "jotai"; import { useEffect, useState } from "react"; import { BsGrid1X2 } from "react-icons/bs"; import RankingListAllTime from "../../components/view/ranking/RankingListAll"; @@ -9,6 +11,7 @@ import RankingListToday from "../../components/view/ranking/RankingListToday"; import RankingListWeekly from "../../components/view/ranking/RankingListWeekly"; import TabNavigation from "../../components/view/ranking/TabNavigation"; import TopicTabs from "../../components/view/ranking/TopicTabs"; +import { openDialogAtom } from "../../lib/atom"; export default function RankingPage() { const [selectedTab, setSelectedTab] = useState< @@ -17,6 +20,7 @@ export default function RankingPage() { const [rankingType, setRankingType] = useState<"nomal" | "image">("nomal"); const [selectedTopic, setSelectedTopic] = useState(0); const [topics, setTopics] = useState([]); + const openDialog = useSetAtom(openDialogAtom); useEffect(() => { const fetchTopics = async () => { @@ -33,8 +37,14 @@ export default function RankingPage() { fetchTopics(); }, []); + // TODO 結果発表のタイミングに修正する。 + useEffect(() => { + openDialog; + }); + return (
+ {rankingType === "nomal" && ( <>
From b4109bf47371b227d5e9a5acb7cf59b933bd6b61 Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Thu, 14 Nov 2024 13:29:21 +0000 Subject: [PATCH 13/19] fix Dialog --- app/src/app/camera/page.tsx | 4 ++-- app/src/app/page.tsx | 4 ++-- app/src/app/ranking/page.tsx | 4 ++-- app/src/components/view/{Dialog.tsx => PointDialog.tsx} | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) rename app/src/components/view/{Dialog.tsx => PointDialog.tsx} (98%) diff --git a/app/src/app/camera/page.tsx b/app/src/app/camera/page.tsx index ad3e8ae..196accd 100644 --- a/app/src/app/camera/page.tsx +++ b/app/src/app/camera/page.tsx @@ -16,7 +16,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Toaster } from "@/components/ui/sonner"; -import { Dialog } from "@/components/view/Dialog"; +import { PointDialog } from "@/components/view/PointDialog"; import { shapeCaption } from "@/functions/shapeCaption"; import { postSimilarity } from "@/functions/simirality"; import type { todayAssignment } from "@/types"; @@ -361,7 +361,7 @@ const CameraApp = () => { <> {isActive ? ( <> - +
- +
{isLoading ? ( diff --git a/app/src/app/ranking/page.tsx b/app/src/app/ranking/page.tsx index 123b6aa..ae39d82 100644 --- a/app/src/app/ranking/page.tsx +++ b/app/src/app/ranking/page.tsx @@ -1,7 +1,7 @@ "use client"; import { Button } from "@/components/ui/button"; -import Dialog from "@/components/view/Dialog"; import { ImageList } from "@/components/view/ranking/ImageList"; +import { PointDialog } from "@/components/view/PointDialog"; import type { todayAssignment } from "@/types"; import { useSetAtom } from "jotai"; import { useEffect, useState } from "react"; @@ -44,7 +44,7 @@ export default function RankingPage() { return (
- + {rankingType === "nomal" && ( <>
diff --git a/app/src/components/view/Dialog.tsx b/app/src/components/view/PointDialog.tsx similarity index 98% rename from app/src/components/view/Dialog.tsx rename to app/src/components/view/PointDialog.tsx index 87fda01..690ef94 100644 --- a/app/src/components/view/Dialog.tsx +++ b/app/src/components/view/PointDialog.tsx @@ -20,7 +20,7 @@ interface NotificationDialogProps { customSubMessage?: string; } -export default function Component({ +export function PointDialog({ type = "login", customTitle, customMessage, From 15c070ab0cf016f69c9710a1d540d270f757fb25 Mon Sep 17 00:00:00 2001 From: Kubosaka Date: Fri, 15 Nov 2024 01:46:16 +0900 Subject: [PATCH 14/19] =?UTF-8?q?=E3=82=B3=E3=83=B3=E3=83=95=E3=83=AA?= =?UTF-8?q?=E3=82=AF=E3=83=88=E8=A7=A3=E6=B6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/package-lock.json | 15 +- app/package.json | 1 + .../continuation/[id]/route.ts | 38 + app/src/app/api/experiencePoint/route.ts | 26 + .../experiencePoint/similarity/[id]/route.ts | 38 + .../api/experiencePoint/speed/[id]/route.ts | 38 + .../api/experiencePoint/total/[id]/route.ts | 38 + app/src/app/api/score/imagelist/route.ts | 83 ++ app/src/app/camera/page.tsx | 838 +++++++++--------- app/src/app/ranking/page.tsx | 67 +- app/src/components/layout/footer.tsx | 100 ++- app/src/components/view/ranking/ImageList.tsx | 89 ++ app/src/components/view/ranking/TopicTabs.tsx | 4 +- 13 files changed, 903 insertions(+), 472 deletions(-) create mode 100644 app/src/app/api/experiencePoint/continuation/[id]/route.ts create mode 100644 app/src/app/api/experiencePoint/route.ts create mode 100644 app/src/app/api/experiencePoint/similarity/[id]/route.ts create mode 100644 app/src/app/api/experiencePoint/speed/[id]/route.ts create mode 100644 app/src/app/api/experiencePoint/total/[id]/route.ts create mode 100644 app/src/app/api/score/imagelist/route.ts create mode 100644 app/src/components/view/ranking/ImageList.tsx diff --git a/app/package-lock.json b/app/package-lock.json index 65447df..e634fb6 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -18,6 +18,7 @@ "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slider": "^1.2.1", "@radix-ui/react-slot": "^1.1.0", + "browser-image-compression": "^2.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -2767,7 +2768,6 @@ "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -4999,6 +4999,14 @@ "node": ">=8" } }, + "node_modules/browser-image-compression": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz", + "integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==", + "dependencies": { + "uzip": "0.20201231.0" + } + }, "node_modules/browser-or-node": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz", @@ -10970,6 +10978,11 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/uzip": { + "version": "0.20201231.0", + "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", + "integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==" + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/app/package.json b/app/package.json index d482bab..e11e0d3 100755 --- a/app/package.json +++ b/app/package.json @@ -22,6 +22,7 @@ "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slider": "^1.2.1", "@radix-ui/react-slot": "^1.1.0", + "browser-image-compression": "^2.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "date-fns": "^4.1.0", diff --git a/app/src/app/api/experiencePoint/continuation/[id]/route.ts b/app/src/app/api/experiencePoint/continuation/[id]/route.ts new file mode 100644 index 0000000..6b137d7 --- /dev/null +++ b/app/src/app/api/experiencePoint/continuation/[id]/route.ts @@ -0,0 +1,38 @@ +import { prisma } from "@lib/prisma"; +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; + +// continuationDayの更新 +export async function PUT(req: NextRequest) { + const { pathname } = new URL(req.url || ""); + const id = Number(pathname.split("/").pop()); + + if (!id) { + return NextResponse.json( + { error: "Invalid or missing ID" }, + { status: 400 }, + ); + } + + try { + const { continuationDay } = await req.json(); + if (typeof continuationDay !== "number") { + return NextResponse.json( + { error: "Invalid speed value" }, + { status: 400 }, + ); + } + + const putContinuationDay = await prisma.experiencePoint.update({ + where: { id }, + data: { continuationDay: continuationDay }, + }); + + return NextResponse.json({ putContinuationDay }, { status: 200 }); + } catch (error) { + return NextResponse.json( + { error: "Failed to update continuationDay", details: error }, + { status: 500 }, + ); + } +} diff --git a/app/src/app/api/experiencePoint/route.ts b/app/src/app/api/experiencePoint/route.ts new file mode 100644 index 0000000..b6daf37 --- /dev/null +++ b/app/src/app/api/experiencePoint/route.ts @@ -0,0 +1,26 @@ +import { prisma } from "@lib/prisma"; +import type { NextRequest } from "next/server"; + +// GETメソッドのハンドラ関数 +export async function GET(req: NextRequest) { + const { searchParams } = new URL(req.url || ""); + const id = searchParams.get("id") || ""; + const numId = Number(id) + + // prismaでユーザを取得 + const exp = await prisma.experiencePoint.findFirst({ + where: { userId: numId }, + }); + + if (!exp) { + return new Response(JSON.stringify({ message: "Not Found" }), { + status: 404, + headers: { "Content-Type": "application/json" }, + }); + } + + return new Response(JSON.stringify(exp), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} diff --git a/app/src/app/api/experiencePoint/similarity/[id]/route.ts b/app/src/app/api/experiencePoint/similarity/[id]/route.ts new file mode 100644 index 0000000..b9a12be --- /dev/null +++ b/app/src/app/api/experiencePoint/similarity/[id]/route.ts @@ -0,0 +1,38 @@ +import { prisma } from "@lib/prisma"; +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; + +// similarityPointの更新 +export async function PUT(req: NextRequest) { + const { pathname } = new URL(req.url || ""); + const id = Number(pathname.split("/").pop()); + + if (!id) { + return NextResponse.json( + { error: "Invalid or missing ID" }, + { status: 400 }, + ); + } + + try { + const { similarity } = await req.json(); + if (typeof similarity !== "number") { + return NextResponse.json( + { error: "Invalid speed value" }, + { status: 400 }, + ); + } + + const putSimilarity = await prisma.experiencePoint.update({ + where: { id }, + data: { similarityPoint: similarity }, + }); + + return NextResponse.json({ putSimilarity }, { status: 200 }); + } catch (error) { + return NextResponse.json( + { error: "Failed to update similarityPoint", details: error }, + { status: 500 }, + ); + } +} diff --git a/app/src/app/api/experiencePoint/speed/[id]/route.ts b/app/src/app/api/experiencePoint/speed/[id]/route.ts new file mode 100644 index 0000000..d4748e4 --- /dev/null +++ b/app/src/app/api/experiencePoint/speed/[id]/route.ts @@ -0,0 +1,38 @@ +import { prisma } from "@lib/prisma"; +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; + +// speedPointの更新 +export async function PUT(req: NextRequest) { + const { pathname } = new URL(req.url || ""); + const id = Number(pathname.split("/").pop()); + + if (!id) { + return NextResponse.json( + { error: "Invalid or missing ID" }, + { status: 400 }, + ); + } + + try { + const { speed } = await req.json(); + if (typeof speed !== "number") { + return NextResponse.json( + { error: "Invalid speed value" }, + { status: 400 }, + ); + } + + const putSpeed = await prisma.experiencePoint.update({ + where: { id }, + data: { speedPoint: speed }, + }); + + return NextResponse.json({ putSpeed }, { status: 200 }); + } catch (error) { + return NextResponse.json( + { error: "Failed to update speedPoint", details: error }, + { status: 500 }, + ); + } +} diff --git a/app/src/app/api/experiencePoint/total/[id]/route.ts b/app/src/app/api/experiencePoint/total/[id]/route.ts new file mode 100644 index 0000000..d566a02 --- /dev/null +++ b/app/src/app/api/experiencePoint/total/[id]/route.ts @@ -0,0 +1,38 @@ +import { prisma } from "@lib/prisma"; +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; + +// totalPointの更新 +export async function PUT(req: NextRequest) { + const { pathname } = new URL(req.url || ""); + const id = Number(pathname.split("/").pop()); + + if (!id) { + return NextResponse.json( + { error: "Invalid or missing ID" }, + { status: 400 }, + ); + } + + try { + const { total } = await req.json(); + if (typeof total !== "number") { + return NextResponse.json( + { error: "Invalid speed value" }, + { status: 400 }, + ); + } + + const putTotal = await prisma.experiencePoint.update({ + where: { id }, + data: { totalPoint: total }, + }); + + return NextResponse.json({ putTotal }, { status: 200 }); + } catch (error) { + return NextResponse.json( + { error: "Failed to update totalPoint", details: error }, + { status: 500 }, + ); + } +} diff --git a/app/src/app/api/score/imagelist/route.ts b/app/src/app/api/score/imagelist/route.ts new file mode 100644 index 0000000..c0d384c --- /dev/null +++ b/app/src/app/api/score/imagelist/route.ts @@ -0,0 +1,83 @@ +import type { RankingScores, ScoreDetail } from "@/types"; +import { prisma } from "@lib/prisma"; + +export async function GET() { + const allScores = await prisma.score.findMany({ + select: { + id: true, + point: true, + answerTime: true, + similarity: true, + assignmentId: true, + userId: true, + imageUrl: true, + createdAt: true, + updatedAt: true, + user: { + select: { + id: true, + uid: true, + name: true, + email: true, + photoUrl: true, + createdAt: true, + updatedAt: true, + }, + }, + assignment: { + select: { + id: true, + wordId: true, + date: true, + createdAt: true, + updatedAt: true, + word: { + select: { + id: true, + english: true, + japanese: true, + difficulty: true, + createdAt: true, + updatedAt: true, + }, + }, + }, + }, + }, + orderBy: { createdAt: "desc" }, + }); + + const scoreDetails: ScoreDetail[] = allScores.map((score) => { + if (!score.assignment) { + return { + id: 0, + assignment: "", + answerIntervalTime: 0, + userName: "", + imageUrl: "", + point: 0, + similarity: 0, + }; + } + + const answerIntervalTimeMilliseconds = + score.answerTime.getTime() - score.assignment.date.getTime(); + const answerIntervalTimeSeconds = answerIntervalTimeMilliseconds / 1000; + const scoreDetail: ScoreDetail = { + id: score.id, + assignment: score.assignment.word.english, + answerIntervalTime: answerIntervalTimeSeconds, + userName: score.user?.name ?? "", + imageUrl: score.imageUrl, + point: score.point, + similarity: score.similarity, + }; + + return scoreDetail; + }); + + return new Response(JSON.stringify(scoreDetails), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} diff --git a/app/src/app/camera/page.tsx b/app/src/app/camera/page.tsx index de2a027..4a225db 100644 --- a/app/src/app/camera/page.tsx +++ b/app/src/app/camera/page.tsx @@ -1,446 +1,482 @@ "use client"; +import { Answered } from "@/components/Answered"; +import { AssignmentBadge } from "@/components/AssignmentBadge"; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Toaster } from "@/components/ui/sonner"; import { shapeCaption } from "@/functions/shapeCaption"; import { postSimilarity } from "@/functions/simirality"; +import type { todayAssignment } from "@/types"; +import imageCompression from "browser-image-compression"; import type React from "react"; import { useEffect, useRef, useState } from "react"; import { Camera, type CameraType } from "react-camera-pro"; +import { toast } from "sonner"; import AddImageIcon from "../../../public/icons/icon-add-image.svg"; import RotateCameraIcon from "../../../public/icons/icon-rotate-camera.svg"; import ShutterIcon from "../../../public/icons/icon-shutter.svg"; -import { toast } from "sonner"; -import { Toaster } from "@/components/ui/sonner"; -import { todayAssignment } from "@/types"; -import { Answered } from "@/components/Answered"; -import { AssignmentBadge } from "@/components/AssignmentBadge"; interface ImagePreviewProps { - image: string | null; - onClick: () => void; + image: string | null; + onClick: () => void; } interface UploadResponse { - url: string; - success: boolean; + url: string; + success: boolean; } interface ScoreData { - similarity: number; - answerTime: Date; - imageUrl: string; - assignmentId: number; - userId: number; + similarity: number; + answerTime: Date; + imageUrl: string; + assignmentId: number; + userId: number; } const BUCKET_NAME = "kz2404"; const ImagePreview = ({ image, onClick }: ImagePreviewProps) => ( -
{ - if (e.key === "Enter" || e.key === " ") { - onClick(); - } - }} - /> +
{ + if (e.key === "Enter" || e.key === " ") { + onClick(); + } + }} + /> ); const DialogImagePreview = ({ image }: { image: string | null }) => { - if (!image) return null; - - return ( -
-
-
- ); + if (!image) return null; + + return ( +
+
+
+ ); }; const LoadingSpinner = () => ( -
-
-

アップロード中...

-
+
+
+

アップロード中...

+
); const imageDataToBase64 = (imageData: ImageData): string => { - const canvas = document.createElement("canvas"); - canvas.width = imageData.width; - canvas.height = imageData.height; + const canvas = document.createElement("canvas"); + canvas.width = imageData.width; + canvas.height = imageData.height; - const ctx = canvas.getContext("2d"); - if (!ctx) throw new Error("Failed to get 2D context"); + const ctx = canvas.getContext("2d"); + if (!ctx) throw new Error("Failed to get 2D context"); - ctx.putImageData(imageData, 0, 0); - return canvas.toDataURL("image/jpeg"); + ctx.putImageData(imageData, 0, 0); + return canvas.toDataURL("image/jpeg"); }; const CameraApp = () => { - const [image, setImage] = useState(null); - const [showImage, setShowImage] = useState(false); - const [isUploading, setIsUploading] = useState(false); - const [showConfirmDialog, setShowConfirmDialog] = useState(false); - const [tempImage, setTempImage] = useState(null); - const camera = useRef(null); - const [devices, setDevices] = useState([]); - const [activeDeviceId, setActiveDeviceId] = useState(undefined); - const [currentDeviceIndex, setCurrentDeviceIndex] = useState(0); - const [todayAssignment, setTodayAssignment] = useState(); - const [assignments, setAssignments] = useState([]); - const [isActive, setIsActive] = useState(true); - - useEffect(() => { - const getDevices = async () => { - const user = localStorage.getItem("userID"); - if (user === null) { - console.error("ユーザー情報が取得できませんでした。"); - return; - } - const userInfo = JSON.parse(user); - const resAssignment = await fetch(`/api/assignment/today?uid=${userInfo?.uid}`); - const assignmentData = await resAssignment.json(); - - if (assignmentData.length === 0) { - setIsActive(false); - return; - } - - const isAnsweredAll = assignmentData.every( - (assignment: todayAssignment) => assignment.isAnswered, - ); - if (isAnsweredAll) { - setIsActive(false); - return; - } - - const notAnsweredAssignment = assignmentData.find( - (assignment: todayAssignment) => !assignment.isAnswered, - ); - - setTodayAssignment(notAnsweredAssignment); - setAssignments(assignmentData); - - if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { - console.error("メディアデバイスAPIがサポートされていません。"); - } - - try { - const devices = await navigator.mediaDevices.enumerateDevices(); - const videoDevices = devices.filter((device) => device.kind === "videoinput"); - setDevices(videoDevices); - if (videoDevices.length > 0) { - setActiveDeviceId(videoDevices[0].deviceId); - } - } catch (error) { - console.error("デバイスの取得中にエラーが発生しました:", error); - } - }; - - getDevices(); - }, []); - - const switchCamera = () => { - if (devices.length > 1) { - const nextIndex = (currentDeviceIndex + 1) % devices.length; - setActiveDeviceId(devices[nextIndex].deviceId); - setCurrentDeviceIndex(nextIndex); - } - }; - - const uploadImage = async ( - imageData: string, - ): Promise<{ imageName: string; data: UploadResponse }> => { - setIsUploading(true); - try { - const base64Response = await fetch(imageData); - const blob = await base64Response.blob(); - - // 拡張子取得 - const Extension = blob.type.split("/")[1]; - - // 日付取得 - const date = new Date(); - const thisMonth = date.getMonth() + 1; - const month = thisMonth < 10 ? "0" + thisMonth : thisMonth; - const day = date.getDate() < 10 ? "0" + date.getDate() : date.getDate(); - const formattedDate = `${date.getFullYear()}${month}${day}`; - - // ランダム文字列を生成する関数 - const generateRandomString = (charCount = 7): string => { - const str = Math.random().toString(36).substring(2).slice(-charCount); - return str.length < charCount ? str + "a".repeat(charCount - str.length) : str; - }; - - const randomStr = generateRandomString(); - // ファイル名作成 - const imageName = `${formattedDate}_${randomStr}.${Extension}`; - - const formData = new FormData(); - formData.append("image", blob, imageName); - - const response = await fetch("/api/minio", { - method: "POST", - body: formData, - }); - - const data = await response.json(); - - return { imageName, data }; - } catch (error) { - console.error("画像のアップロードに失敗しました:", error); - throw error; - } - }; - - const getCaption = async (imageName: string): Promise<{ caption: string }> => { - try { - const response = await fetch(`/api/image?imageName=${imageName}`); - if (!response.ok) { - throw new Error("キャプションの取得に失敗しました"); - } - - return await response.json(); - } catch (error) { - console.error("キャプションの取得に失敗しました:", error); - throw error; - } - }; - - // スコア計算を行います。 - const similarityRequest = async (caption: string) => { - const words: string[] = shapeCaption(caption); - const assignmentWord: string = todayAssignment?.english || ""; - const resSimilarity = await postSimilarity(assignmentWord, words); - return { - similarity: resSimilarity.similarity as number, - assignmentId: todayAssignment?.assignmentId as number, - }; - }; - - // userIdの取得 - const getUserId = async () => { - const userString = localStorage.getItem("userID"); - if (userString === null) { - return null; - } - const userData = JSON.parse(userString); - - const resUserId = await fetch("/api/user?uid=" + userData.uid, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - return await resUserId.json(); - }; - - // scoreの送信 - const submitScore = async (scoreData: ScoreData) => { - const response = await fetch("/api/score", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ scoreData }), - }); - - if (!response.ok) { - console.error("スコアの送信に失敗しました", response.statusText); - return; - } - - return await response.json(); - }; - - const handleConfirm = async () => { - if (tempImage) { - try { - const { imageName } = await uploadImage(tempImage); - const imageURL = `${process.env.NEXT_PUBLIC_MINIO_ENDPOINT}${BUCKET_NAME}/${imageName}`; - - const res = await getCaption(imageName); - const caption = res.caption; - - setShowConfirmDialog(false); - setImage(tempImage); - setShowImage(true); - setTempImage(null); - - const { similarity, assignmentId } = await similarityRequest(caption); - - const user = await getUserId(); - const userId: number = user.id; - - const scoreData: ScoreData = { - similarity: similarity, - answerTime: new Date(), - imageUrl: imageURL, - assignmentId: assignmentId, - userId: userId, - }; - const response = await submitScore(scoreData); - const score = response.score; - const percentSimilarity = Math.floor(similarity * 100); - const message = `${caption} 類似度 ${percentSimilarity}% スコア: ${score.point} ランキングから順位を確認しましょう!`; - const newAssignments = assignments.map((assignment) => { - if (assignment.assignmentId === assignmentId) { - assignment.isAnswered = true; - } - return assignment; - }); - const notAnsweredAssignment = newAssignments.find( - (assignment: todayAssignment) => !assignment.isAnswered, - ); - - setTodayAssignment(notAnsweredAssignment); - - setIsUploading(false); - toast(message); - setAssignments(newAssignments); - - if (newAssignments.every((assignment) => assignment.isAnswered)) { - setIsActive(false); - } - - - } catch (error) { - setIsUploading(false); - console.error("アップロード中にエラーが発生しました:", error); - } - } - }; - - const handleCancel = () => { - setShowConfirmDialog(false); - setTempImage(null); - }; - - const handleImageCapture = (capturedImage: string | ImageData) => { - const imageStr = - capturedImage instanceof ImageData ? imageDataToBase64(capturedImage) : capturedImage; - - setTempImage(imageStr); - setShowConfirmDialog(true); - }; - - return ( - <> - {isActive ? ( - <> -
- -
-
-
- - { - const file = event.target.files?.[0]; - if (file) { - const reader = new FileReader(); - reader.onload = () => { - handleImageCapture(reader.result as string); - }; - reader.readAsDataURL(file); - } - }} - /> -
- - -
- - - - - 画像のアップロード確認 - - この画像をアップロードしてもよろしいですか? - - - - - - - いいえ - はい - - - - {isUploading && } - - {todayAssignment?.english && ( - - )} - - ) : ( - - )} - - ); + const [image, setImage] = useState(null); + const [showImage, setShowImage] = useState(false); + const [isUploading, setIsUploading] = useState(false); + const [showConfirmDialog, setShowConfirmDialog] = useState(false); + const [tempImage, setTempImage] = useState(null); + const camera = useRef(null); + const [devices, setDevices] = useState([]); + const [activeDeviceId, setActiveDeviceId] = useState( + undefined + ); + const [currentDeviceIndex, setCurrentDeviceIndex] = useState(0); + const [todayAssignment, setTodayAssignment] = useState< + todayAssignment | undefined + >(); + const [assignments, setAssignments] = useState([]); + const [isActive, setIsActive] = useState(true); + + useEffect(() => { + const getDevices = async () => { + const user = localStorage.getItem("userID"); + if (user === null) { + console.error("ユーザー情報が取得できませんでした。"); + return; + } + const userInfo = JSON.parse(user); + const resAssignment = await fetch( + `/api/assignment/today?uid=${userInfo?.uid}` + ); + const assignmentData = await resAssignment.json(); + + if (assignmentData.length === 0) { + setIsActive(false); + return; + } + + const isAnsweredAll = assignmentData.every( + (assignment: todayAssignment) => assignment.isAnswered + ); + if (isAnsweredAll) { + setIsActive(false); + return; + } + + const notAnsweredAssignment = assignmentData.find( + (assignment: todayAssignment) => !assignment.isAnswered + ); + + setTodayAssignment(notAnsweredAssignment); + setAssignments(assignmentData); + + if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { + console.error("メディアデバイスAPIがサポートされていません。"); + } + + try { + const devices = await navigator.mediaDevices.enumerateDevices(); + const videoDevices = devices.filter( + (device) => device.kind === "videoinput" + ); + setDevices(videoDevices); + if (videoDevices.length > 0) { + setActiveDeviceId(videoDevices[0].deviceId); + } + } catch (error) { + console.error("デバイスの取得中にエラーが発生しました:", error); + } + }; + + getDevices(); + }, []); + + const switchCamera = () => { + if (devices.length > 1) { + const nextIndex = (currentDeviceIndex + 1) % devices.length; + setActiveDeviceId(devices[nextIndex].deviceId); + setCurrentDeviceIndex(nextIndex); + } + }; + + const uploadImage = async ( + imageData: string + ): Promise<{ imageName: string; data: UploadResponse }> => { + setIsUploading(true); + try { + const base64Response = await fetch(imageData); + const originalBlob = await base64Response.blob(); + + const compressOptions = { + maxSizeMB: 0.01, + maxWidthOrHeight: 1920, + useWebWorker: true, + }; + + const originalFile = new File([originalBlob], "tempImage", { + type: originalBlob.type, + }); + const compressedBlob = await imageCompression( + originalFile, + compressOptions + ); + + // 拡張子取得 + const Extension = compressedBlob.type.split("/")[1]; + + // 日付取得 + const date = new Date(); + const thisMonth = date.getMonth() + 1; + const month = thisMonth < 10 ? `0${thisMonth}` : thisMonth; + const day = date.getDate() < 10 ? `0${date.getDate()}` : date.getDate(); + const formattedDate = `${date.getFullYear()}${month}${day}`; + + // ランダム文字列を生成する関数 + const generateRandomString = (charCount = 7): string => { + const str = Math.random().toString(36).substring(2).slice(-charCount); + return str.length < charCount + ? str + "a".repeat(charCount - str.length) + : str; + }; + + const randomStr = generateRandomString(); + // ファイル名作成 + const imageName = `${formattedDate}_${randomStr}.${Extension}`; + + const formData = new FormData(); + formData.append("image", compressedBlob, imageName); + + const response = await fetch("/api/minio", { + method: "POST", + body: formData, + }); + + const data = await response.json(); + + return { imageName, data }; + } catch (error) { + console.error("画像のアップロードに失敗しました:", error); + throw error; + } + }; + + const getCaption = async ( + imageName: string + ): Promise<{ caption: string }> => { + try { + const response = await fetch(`/api/image?imageName=${imageName}`); + if (!response.ok) { + throw new Error("キャプションの取得に失敗しました"); + } + + return await response.json(); + } catch (error) { + console.error("キャプションの取得に失敗しました:", error); + throw error; + } + }; + + // スコア計算を行います。 + const similarityRequest = async (caption: string) => { + const words: string[] = shapeCaption(caption); + const assignmentWord: string = todayAssignment?.english || ""; + const resSimilarity = await postSimilarity(assignmentWord, words); + return { + similarity: resSimilarity.similarity as number, + assignmentId: todayAssignment?.assignmentId as number, + }; + }; + + // userIdの取得 + const getUserId = async () => { + const userString = localStorage.getItem("userID"); + if (userString === null) { + return null; + } + const userData = JSON.parse(userString); + + const resUserId = await fetch(`/api/user?uid=${userData.uid}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + return await resUserId.json(); + }; + + // scoreの送信 + const submitScore = async (scoreData: ScoreData) => { + const response = await fetch("/api/score", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ scoreData }), + }); + + if (!response.ok) { + console.error("スコアの送信に失敗しました", response.statusText); + return; + } + + return await response.json(); + }; + + const handleConfirm = async () => { + if (tempImage) { + try { + const { imageName } = await uploadImage(tempImage); + const imageURL = `${process.env.NEXT_PUBLIC_MINIO_ENDPOINT}${BUCKET_NAME}/${imageName}`; + + const res = await getCaption(imageName); + const caption = res.caption; + + setShowConfirmDialog(false); + setImage(tempImage); + setShowImage(true); + setTempImage(null); + + const { similarity, assignmentId } = await similarityRequest(caption); + + const user = await getUserId(); + const userId: number = user.id; + + const scoreData: ScoreData = { + similarity: similarity, + answerTime: new Date(), + imageUrl: imageURL, + assignmentId: assignmentId, + userId: userId, + }; + const response = await submitScore(scoreData); + const score = response.score; + const percentSimilarity = Math.floor(similarity * 100); + const message = `${caption} 類似度 ${percentSimilarity}% スコア: ${score.point} ランキングから順位を確認しましょう!`; + const newAssignments = assignments.map((assignment) => { + if (assignment.assignmentId === assignmentId) { + assignment.isAnswered = true; + } + return assignment; + }); + const notAnsweredAssignment = newAssignments.find( + (assignment: todayAssignment) => !assignment.isAnswered + ); + + setTodayAssignment(notAnsweredAssignment); + + setIsUploading(false); + toast(message); + setAssignments(newAssignments); + + if (newAssignments.every((assignment) => assignment.isAnswered)) { + setIsActive(false); + } + } catch (error) { + setIsUploading(false); + console.error("アップロード中にエラーが発生しました:", error); + } + } + }; + + const handleCancel = () => { + setShowConfirmDialog(false); + setTempImage(null); + }; + + const handleImageCapture = (capturedImage: string | ImageData) => { + const imageStr = + capturedImage instanceof ImageData + ? imageDataToBase64(capturedImage) + : capturedImage; + + setTempImage(imageStr); + setShowConfirmDialog(true); + }; + + return ( + <> + {isActive ? ( + <> +
+ +
+
+
+ + { + const file = event.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = () => { + handleImageCapture(reader.result as string); + }; + reader.readAsDataURL(file); + } + }} + /> +
+ + +
+ + + + + + 画像のアップロード確認 + + + この画像をアップロードしてもよろしいですか? + + + + + + + + いいえ + + + はい + + + + + {isUploading && } + + {todayAssignment?.english && ( + + )} + + ) : ( + + )} + + ); }; export default CameraApp; diff --git a/app/src/app/ranking/page.tsx b/app/src/app/ranking/page.tsx index aebae6a..d538097 100644 --- a/app/src/app/ranking/page.tsx +++ b/app/src/app/ranking/page.tsx @@ -1,6 +1,9 @@ "use client"; +import { Button } from "@/components/ui/button"; +import { ImageList } from "@/components/view/ranking/ImageList"; import type { todayAssignment } from "@/types"; import { useEffect, useState } from "react"; +import { BsGrid1X2 } from "react-icons/bs"; import RankingListAllTime from "../../components/view/ranking/RankingListAll"; import RankingListToday from "../../components/view/ranking/RankingListToday"; import RankingListWeekly from "../../components/view/ranking/RankingListWeekly"; @@ -11,6 +14,7 @@ export default function RankingPage() { const [selectedTab, setSelectedTab] = useState< "today" | "weekly" | "allTime" >("today"); + const [rankingType, setRankingType] = useState<"nomal" | "image">("nomal"); const [selectedTopic, setSelectedTopic] = useState(0); const [topics, setTopics] = useState([]); @@ -31,26 +35,49 @@ export default function RankingPage() { return (
-
- - {selectedTab === "today" && ( - - )} -
-
- {selectedTab === "today" && ( - - )} - {selectedTab === "weekly" && } - {selectedTab === "allTime" && } -
+ {rankingType === "nomal" && ( + <> +
+ + {selectedTab === "today" && ( +
+ + +
+ )} +
+
+ {selectedTab === "today" && ( + + )} + {selectedTab === "weekly" && } + {selectedTab === "allTime" && } +
+ + )} + {rankingType === "image" && ( +
+ +
+ )}
); } diff --git a/app/src/components/layout/footer.tsx b/app/src/components/layout/footer.tsx index ced1749..d56d054 100644 --- a/app/src/components/layout/footer.tsx +++ b/app/src/components/layout/footer.tsx @@ -9,57 +9,61 @@ import RankingIcon from "../../../public/icons/icon-ranking.svg"; import UserIcon from "../../../public/icons/icon-user.svg"; const Footer = () => { - const [activeButton, setActiveButton] = useState(null); - const router = useRouter(); - const pathname = usePathname(); + const [activeButton, setActiveButton] = useState(null); + const router = useRouter(); + const pathname = usePathname(); - useEffect(() => { - if (pathname === "/camera") setActiveButton("camera"); - else if (pathname === "/") setActiveButton("theme"); - else if (pathname === "/ranking") setActiveButton("ranking"); - else if (pathname === "/user") setActiveButton("user"); - }, [pathname]); + useEffect(() => { + if (pathname === "/camera") setActiveButton("camera"); + else if (pathname === "/") setActiveButton("theme"); + else if (pathname === "/ranking") setActiveButton("ranking"); + else if (pathname === "/user") setActiveButton("user"); + }, [pathname]); - const handleClick = (path: string) => { - router.push(path); - }; + const handleClick = (path: string) => { + router.push(path); + }; - return ( -
- - - - -
- ); + return ( + + ); }; export default Footer; diff --git a/app/src/components/view/ranking/ImageList.tsx b/app/src/components/view/ranking/ImageList.tsx new file mode 100644 index 0000000..2af960a --- /dev/null +++ b/app/src/components/view/ranking/ImageList.tsx @@ -0,0 +1,89 @@ +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import type { ScoreDetail } from "@/types"; +import { useEffect, useState } from "react"; +import { VscListOrdered } from "react-icons/vsc"; + +const LoadingSpinner = () => ( +
+
+

読み込み中...

+
+); + +type ImageListProps = { + setMode: (mode: "nomal" | "image") => void; +}; + +const MODE = { + NOMAL: "nomal", + IMAGE: "image", +} as const; + +export const ImageList = ({ setMode }: ImageListProps) => { + const [isLoading, setIsLoading] = useState(true); + + const [scoreDetails, setScoreDetails] = useState([]); + useEffect(() => { + const fetchData = () => { + setIsLoading(true); + return fetch("/api/score/imagelist") + .then((response) => { + if (!response.ok) { + throw new Error("Failed to fetch data"); + } + return response.json(); + }) + .then((result) => setScoreDetails(result)) + .catch((error) => console.error("Error fetching data:", error)) + .finally(() => setIsLoading(false)); + }; + + fetchData(); + }, []); + + if (isLoading) { + return ; + } + + return ( +
+
+ +
+
+ {scoreDetails.map((score) => ( +
+ +
+ ))} +
+
+ ); +}; + +const ImageBox = ({ score }: { score: ScoreDetail }) => { + return ( + + 画像 +

+ {score.assignment} +

+
+ ); +}; diff --git a/app/src/components/view/ranking/TopicTabs.tsx b/app/src/components/view/ranking/TopicTabs.tsx index db1f23e..184488c 100644 --- a/app/src/components/view/ranking/TopicTabs.tsx +++ b/app/src/components/view/ranking/TopicTabs.tsx @@ -11,10 +11,10 @@ interface TopicTabsProps { const TopicTabs: React.FC = ({ selectedTopic, setSelectedTopic, - topics, + topics, }) => { return ( -
+
{topics.map((topic) => (
) : ( myScore.map((score) => ( -
+
チャレンジ画像("nomal"); const [selectedTopic, setSelectedTopic] = useState(0); const [topics, setTopics] = useState([]); - const openDialog = useSetAtom(openDialogAtom); + const [isPointDialogOpen, setIsPointDialogOpen] = usePointDialogOpen(); + + const handleClickOpen = () => { + setIsPointDialogOpen(true); + }; useEffect(() => { const fetchTopics = async () => { @@ -39,12 +42,12 @@ export default function RankingPage() { // TODO 結果発表のタイミングに修正する。 useEffect(() => { - openDialog; + handleClickOpen(); }); return (
- + {isPointDialogOpen && } {rankingType === "nomal" && ( <>
diff --git a/app/src/components/view/PointDialog.tsx b/app/src/components/view/PointDialog.tsx index 690ef94..88026fe 100644 --- a/app/src/components/view/PointDialog.tsx +++ b/app/src/components/view/PointDialog.tsx @@ -10,8 +10,8 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { useAtom } from "jotai"; -import { dialogOpenAtom } from "../../lib/atom"; +import { useState } from "react"; +import { useHasShownOnce } from "../../lib/atom"; interface NotificationDialogProps { type: "login" | "photo" | "ranking"; @@ -26,7 +26,8 @@ export function PointDialog({ customMessage, customSubMessage, }: NotificationDialogProps) { - const [isOpen, setIsOpen] = useAtom(dialogOpenAtom); + const [hasShownOnce, _] = useHasShownOnce(); + const [isOpen, setIsOpen] = useState(hasShownOnce); const content = { login: { From 28ddcfe64c117a17f22563891382318666846ab8 Mon Sep 17 00:00:00 2001 From: nose221834 Date: Fri, 15 Nov 2024 03:37:09 +0900 Subject: [PATCH 18/19] =?UTF-8?q?import=20=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E3=81=AE=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/app/camera/page.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/app/camera/page.tsx b/app/src/app/camera/page.tsx index 1a56957..9042e28 100644 --- a/app/src/app/camera/page.tsx +++ b/app/src/app/camera/page.tsx @@ -20,7 +20,6 @@ import { PointDialog } from "@/components/view/PointDialog"; import { shapeCaption } from "@/functions/shapeCaption"; import { postSimilarity } from "@/functions/simirality"; import { usePointDialogOpen } from "@/lib/atom"; -import type { todayAssignment } from "@/types"; import type { ScoreResponse, User, todayAssignment } from "@/types"; import imageCompression from "browser-image-compression"; import type React from "react"; From d11daa5f96f6bb5fc718fbf4d19ec9c4f19d7104 Mon Sep 17 00:00:00 2001 From: nose221834 Date: Fri, 15 Nov 2024 03:37:19 +0900 Subject: [PATCH 19/19] =?UTF-8?q?TODO=E3=82=B3=E3=83=A1=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/app/page.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/app/page.tsx b/app/src/app/page.tsx index 01ac64a..9e5dcef 100755 --- a/app/src/app/page.tsx +++ b/app/src/app/page.tsx @@ -21,6 +21,8 @@ export default function Home() { const [_, setIsPointDialogOpen] = usePointDialogOpen(); const [hasShownOnce, setHasShownOnce] = useHasShownOnce(); + // TODO:ログインモーダルが表示されるようにする + useEffect(() => { const fetchData = async () => { const userIdString = localStorage.getItem("userID");