diff --git a/package-lock.json b/package-lock.json index c27bbe4e..63852b35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "react-messenger-19th", "version": "0.1.0", "dependencies": { + "@heroicons/react": "^2.1.5", + "@tanstack/react-query": "^5.59.16", + "@tanstack/react-query-devtools": "^5.59.16", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -17,9 +20,19 @@ "@types/react-dom": "^18.2.22", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-icons": "^5.3.0", + "react-query": "^3.39.3", + "react-router-dom": "^6.26.2", "react-scripts": "5.0.1", + "recoil": "^0.7.7", + "styled-components": "^6.1.13", "typescript": "^4.9.5", "web-vitals": "^2.1.4" + }, + "devDependencies": { + "@types/react-icons": "^2.2.7", + "@types/styled-components": "^5.1.34", + "tailwindcss": "^3.4.13" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2288,6 +2301,27 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -2381,6 +2415,15 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@heroicons/react": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.1.5.tgz", + "integrity": "sha512-FuzFN+BsHa+7OxbvAERtgBTNeZpUjgM/MIizfVkSCL2/edriN0Hx/DWRCR//aPYwO5QX/YlgLGXk+E3PcfZwjA==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -3338,6 +3381,15 @@ } } }, + "node_modules/@remix-run/router": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz", + "integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3656,6 +3708,59 @@ "url": "https://github.com/sponsors/gregberge" } }, + "node_modules/@tanstack/query-core": { + "version": "5.59.16", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.16.tgz", + "integrity": "sha512-crHn+G3ltqb5JG0oUv6q+PMz1m1YkjpASrXTU+sYWW9pLk0t2GybUHNRqYPZWhxgjPaVGC4yp92gSFEJgYEsPw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.58.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.58.0.tgz", + "integrity": "sha512-iFdQEFXaYYxqgrv63ots+65FGI+tNp5ZS5PdMU1DWisxk3fez5HG3FyVlbUva+RdYS5hSLbxZ9aw3yEs97GNTw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.59.16", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.59.16.tgz", + "integrity": "sha512-MuyWheG47h6ERd4PKQ6V8gDyBu3ThNG22e1fRVwvq6ap3EqsFhyuxCAwhNP/03m/mLg+DAb0upgbPaX6VB+CkQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.59.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.59.16", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.59.16.tgz", + "integrity": "sha512-Dejo39QBXmDqXZ3vdrk7vHDvs7TvL573/AX2NveMBmRAufAPYuE3oWSKP/gGqkDfEqyr4CmldOj+v9cKskUchQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.58.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.59.16", + "react": "^18 || ^19" + } + }, "node_modules/@testing-library/dom": { "version": "9.3.4", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", @@ -4103,6 +4208,17 @@ "@types/node": "*" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -4227,6 +4343,27 @@ "@types/react": "*" } }, + "node_modules/@types/react-icon-base": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@types/react-icon-base/-/react-icon-base-2.1.6.tgz", + "integrity": "sha512-ebbN1JjCm6RxBd3HdI1+8VCdiOI4qMjnl9DIHWJFrB/eYLF4mzIgdL34PIqCJBLY3vlwil9v6IHQvzsa8vgMsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-icons": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@types/react-icons/-/react-icons-2.2.7.tgz", + "integrity": "sha512-qxc8xtwgDG5Ub/WILU9tZa7zxz2UZqOU4yXbBa+Xg+0LbP031NB9gvf1d/ALvHLGCsCf3WEVttNoW/wc30jn1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@types/react-icon-base": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -4290,6 +4427,24 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" }, + "node_modules/@types/styled-components": { + "version": "5.1.34", + "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.34.tgz", + "integrity": "sha512-mmiVvwpYklFIv9E8qfxuPyIt/OuyIrn6gMOAMOFUO3WJfSrSE+sGUoa4PiZj77Ut7bKZpaa6o1fBKS/4TOEvnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hoist-non-react-statics": "*", + "@types/react": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", + "license": "MIT" + }, "node_modules/@types/testing-library__jest-dom": { "version": "5.14.9", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz", @@ -5515,6 +5670,15 @@ "node": ">= 8.0.0" } }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -5628,6 +5792,22 @@ "node": ">=8" } }, + "node_modules/broadcast-channel": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz", + "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "detect-node": "^2.1.0", + "js-sha3": "0.8.0", + "microseconds": "0.2.0", + "nano-time": "1.0.0", + "oblivious-set": "1.0.0", + "rimraf": "3.0.2", + "unload": "2.2.0" + } + }, "node_modules/browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -5750,6 +5930,15 @@ "node": ">= 6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -5762,9 +5951,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001600", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz", - "integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==", + "version": "1.0.30001664", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz", + "integrity": "sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g==", "funding": [ { "type": "opencollective", @@ -5778,7 +5967,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/case-sensitive-paths-webpack-plugin": { "version": "2.4.0", @@ -6182,6 +6372,15 @@ "postcss": "^8.4" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, "node_modules/css-declaration-sorter": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", @@ -6372,6 +6571,17 @@ "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css-tree": { "version": "1.0.0-alpha.37", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", @@ -8861,6 +9071,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hamt_plus": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", + "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==", + "license": "MIT" + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -8953,6 +9169,23 @@ "he": "bin/he" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -11918,6 +12151,12 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -12310,6 +12549,16 @@ "tmpl": "1.0.5" } }, + "node_modules/match-sorter": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz", + "integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.8", + "remove-accents": "0.5.0" + } + }, "node_modules/mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", @@ -12372,6 +12621,12 @@ "node": ">=8.6" } }, + "node_modules/microseconds": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", + "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==", + "license": "MIT" + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -12556,6 +12811,15 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nano-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", + "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==", + "license": "ISC", + "dependencies": { + "big-integer": "^1.6.16" + } + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -12836,6 +13100,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oblivious-set": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", + "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==", + "license": "MIT" + }, "node_modules/obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", @@ -14851,11 +15121,46 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-icons": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz", + "integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-query": { + "version": "3.39.3", + "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz", + "integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "broadcast-channel": "^3.4.1", + "match-sorter": "^6.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -14864,6 +15169,38 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz", + "integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.19.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz", + "integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.19.2", + "react-router": "6.26.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -14968,6 +15305,26 @@ "node": ">=8.10.0" } }, + "node_modules/recoil": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", + "integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==", + "license": "MIT", + "dependencies": { + "hamt_plus": "1.0.2" + }, + "peerDependencies": { + "react": ">=16.13.1" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -15105,6 +15462,12 @@ "node": ">= 0.10" } }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==", + "license": "MIT" + }, "node_modules/renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", @@ -15719,6 +16082,12 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -16260,6 +16629,34 @@ "webpack": "^5.0.0" } }, + "node_modules/styled-components": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.13.tgz", + "integrity": "sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.38", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, "node_modules/stylehacks": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", @@ -16275,6 +16672,12 @@ "postcss": "^8.2.15" } }, + "node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -16491,9 +16894,10 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, "node_modules/tailwindcss": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", - "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz", + "integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==", + "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -16503,7 +16907,7 @@ "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.19.1", + "jiti": "^1.21.0", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", @@ -17021,6 +17425,16 @@ "node": ">= 10.0.0" } }, + "node_modules/unload": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", + "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.6.2", + "detect-node": "^2.0.4" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index ea335d36..9e67d056 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "0.1.0", "private": true, "dependencies": { + "@heroicons/react": "^2.1.5", + "@tanstack/react-query": "^5.59.16", + "@tanstack/react-query-devtools": "^5.59.16", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -12,7 +15,12 @@ "@types/react-dom": "^18.2.22", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-icons": "^5.3.0", + "react-query": "^3.39.3", + "react-router-dom": "^6.26.2", "react-scripts": "5.0.1", + "recoil": "^0.7.7", + "styled-components": "^6.1.13", "typescript": "^4.9.5", "web-vitals": "^2.1.4" }, @@ -39,5 +47,10 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "@types/react-icons": "^2.2.7", + "@types/styled-components": "^5.1.34", + "tailwindcss": "^3.4.13" } } diff --git a/public/Profile_image.svg b/public/Profile_image.svg new file mode 100644 index 00000000..d8b7c110 --- /dev/null +++ b/public/Profile_image.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/fakedata/messagesData.json b/public/fakedata/messagesData.json new file mode 100644 index 00000000..9cfe904a --- /dev/null +++ b/public/fakedata/messagesData.json @@ -0,0 +1,42 @@ +[ + { + "id": 1, + "text": "머하밍??", + "sender": "CEOS", + "receiver": "김류원", + "time": "2024-10-31T12:34:56.000Z", + "chatRoomId": "CEOS" + }, + { + "id": 2, + "text": "과제중🤮🤮", + "sender": "김류원", + "receiver": "CEOS", + "time": "2024-10-31T12:35:56.000Z", + "chatRoomId": "CEOS" + }, + { + "id": 3, + "text": "🤦‍♀️🤦‍♀️", + "sender": "CEOS", + "receiver": "김류원", + "time": "2024-10-31T12:36:56.000Z", + "chatRoomId": "CEOS" + }, + { + "id": 4, + "text": "차은우가 보낸 메세지", + "sender": "차은우", + "receiver": "김류원", + "time": "2024-10-31T12:37:56.000Z", + "chatRoomId": "차은우" + }, + { + "id": 5, + "text": "뷔가 보낸 메세지", + "sender": "뷔", + "receiver": "김류원", + "time": "2024-10-31T12:38:56.000Z", + "chatRoomId": "뷔" + } +] diff --git a/public/fakedata/users.json b/public/fakedata/users.json new file mode 100644 index 00000000..a9116694 --- /dev/null +++ b/public/fakedata/users.json @@ -0,0 +1,50 @@ +[ + { + "id": 0, + "name": "김류원", + "profilePic": "/Profile_image.svg", + "facebook": "https://www.facebook.com/", + "instagram": "https://www.instagram.com/" + }, + { + "id": 1, + "name": "CEOS", + "profilePic": "/Profile_image.svg", + "facebook": "https://www.facebook.com/clubceos/", + "instagram": "https://www.instagram.com/ceos.sinchon/", + "description": "IT 연합 동아리" + }, + + { + "id": 2, + "name": "차은우", + "profilePic": "/Profile_image.svg", + "facebook": "https://www.facebook.com/", + "instagram": "https://www.instagram.com/", + "description": "차은우! 차은우! 차은우!" + }, + { + "id": 3, + "name": "송강", + "profilePic": "/Profile_image.svg", + "facebook": "https://www.facebook.com/", + "instagram": "https://www.instagram.com/", + "description": "스위트홈에서 잘생김" + }, + { + "id": 4, + "name": "뷔", + "profilePic": "/Profile_image.svg", + "facebook": "https://www.facebook.com/", + "instagram": "https://www.instagram.com/", + "description": "그냥 잘생김" + }, + { + "id": 5, + "name": "서강준", + "profilePic": "/Profile_image.svg", + "facebook": "https://www.facebook.com/", + "instagram": "https://www.instagram.com/", + "description": "정글의법칙에서도 잘생김" + } +] diff --git a/public/index.html b/public/index.html index aa069f27..64044dc1 100644 --- a/public/index.html +++ b/public/index.html @@ -24,6 +24,11 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> + + React App diff --git "a/src/\bimages.d.ts" "b/src/\bimages.d.ts" new file mode 100644 index 00000000..1a3dd3c2 --- /dev/null +++ "b/src/\bimages.d.ts" @@ -0,0 +1,4 @@ +declare module "*.svg" { + const content: any; + export default content; +} diff --git a/src/App.css b/src/App.css index 74b5e053..8c2418e4 100644 --- a/src/App.css +++ b/src/App.css @@ -1,38 +1,3 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } +body { + font-family: "Pretendard"; } diff --git a/src/App.tsx b/src/App.tsx index 5381007b..282d371b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,36 @@ +import { + BrowserRouter as Router, + Routes, + Route, + useMatch, +} from "react-router-dom"; +import ChatRoom from "./pages/ChatRoom"; +import ProfileDetail from "./pages/ProfileDetail"; +import { useState } from "react"; +import BottomNav from "./components/BottomNav"; +import { ReactQueryDevtools } from "react-query/devtools"; + +import Home from "./pages/Home"; +import ChatList from "./pages/ChatList"; +import NotFound from "./pages/NotFound"; + function App() { + const chatRoomMatch = useMatch("/chat-room/:sender"); + return ( -
-

20기 프론트엔드 파이팅!!! 디자인과 사이좋게 지내요~~~

-
+ <> + + } /> + } /> + } /> + } /> + +
+ {chatRoomMatch ? null : } +
+ + + ); } diff --git a/src/api.tsx b/src/api.tsx new file mode 100644 index 00000000..e3c2aafc --- /dev/null +++ b/src/api.tsx @@ -0,0 +1,9 @@ +export function fetchUsers() { + return fetch("/fakedata/users.json").then((response) => response.json()); +} + +export function fetchMessages() { + return fetch("/fakedata/messagesData.json").then((response) => + response.json() + ); +} diff --git a/src/assets/Back.svg b/src/assets/Back.svg new file mode 100644 index 00000000..f91592ac --- /dev/null +++ b/src/assets/Back.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/Story.svg b/src/assets/Story.svg new file mode 100644 index 00000000..a8fbb766 --- /dev/null +++ b/src/assets/Story.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/call.svg b/src/assets/call.svg new file mode 100644 index 00000000..32fabac9 --- /dev/null +++ b/src/assets/call.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/cancel.svg b/src/assets/cancel.svg new file mode 100644 index 00000000..a80b9a99 --- /dev/null +++ b/src/assets/cancel.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/chat.svg b/src/assets/chat.svg new file mode 100644 index 00000000..90959999 --- /dev/null +++ b/src/assets/chat.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/edit.svg b/src/assets/edit.svg new file mode 100644 index 00000000..863e59dc --- /dev/null +++ b/src/assets/edit.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/home.svg b/src/assets/home.svg new file mode 100644 index 00000000..f995d1f4 --- /dev/null +++ b/src/assets/home.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/person_plus.svg b/src/assets/person_plus.svg new file mode 100644 index 00000000..8deec695 --- /dev/null +++ b/src/assets/person_plus.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/plus.svg b/src/assets/plus.svg new file mode 100644 index 00000000..aed87c48 --- /dev/null +++ b/src/assets/plus.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/profile_facebook.svg b/src/assets/profile_facebook.svg new file mode 100644 index 00000000..7752dde1 --- /dev/null +++ b/src/assets/profile_facebook.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/profile_insta.svg b/src/assets/profile_insta.svg new file mode 100644 index 00000000..5ea81cc8 --- /dev/null +++ b/src/assets/profile_insta.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/profile_plus.svg b/src/assets/profile_plus.svg new file mode 100644 index 00000000..fd5af288 --- /dev/null +++ b/src/assets/profile_plus.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/search.svg b/src/assets/search.svg new file mode 100644 index 00000000..5e4f8c33 --- /dev/null +++ b/src/assets/search.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/send.svg b/src/assets/send.svg new file mode 100644 index 00000000..73d2f19b --- /dev/null +++ b/src/assets/send.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/setting.svg b/src/assets/setting.svg new file mode 100644 index 00000000..854934c7 --- /dev/null +++ b/src/assets/setting.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/smile.svg b/src/assets/smile.svg new file mode 100644 index 00000000..78750a3c --- /dev/null +++ b/src/assets/smile.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/thumb_up.svg b/src/assets/thumb_up.svg new file mode 100644 index 00000000..11ea2287 --- /dev/null +++ b/src/assets/thumb_up.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/video.svg b/src/assets/video.svg new file mode 100644 index 00000000..96a987c1 --- /dev/null +++ b/src/assets/video.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/BottomNav.tsx b/src/components/BottomNav.tsx new file mode 100644 index 00000000..3e019a9f --- /dev/null +++ b/src/components/BottomNav.tsx @@ -0,0 +1,74 @@ +import { useState } from "react"; +import { + useLocation, + useMatch, + useNavigate, + useParams, +} from "react-router-dom"; +import chat from "../assets/chat.svg"; +import story from "../assets/Story.svg"; +import home from "../assets/home.svg"; + +const BottomNav = () => { + const location = useLocation(); + const nav = useNavigate(); + + const isHomeActive = location.pathname === "/"; + const isChatActive = location.pathname === "/chat-list"; + const isStoryActive = location.pathname === "/story"; + + const activeFilter = + "invert(29%) sepia(92%) saturate(1429%) hue-rotate(188deg) brightness(95%) contrast(93%)"; + + return ( +
+
+ + + +
+
+ ); +}; + +export default BottomNav; diff --git a/src/components/CHeader.tsx b/src/components/CHeader.tsx new file mode 100644 index 00000000..4ed2c96d --- /dev/null +++ b/src/components/CHeader.tsx @@ -0,0 +1,70 @@ +import backIcon from "../assets/Back.svg"; +import call from "../assets/call.svg"; +import video from "../assets/video.svg"; +import search from "../assets/search.svg"; +import { Navigate, useNavigate } from "react-router-dom"; + +interface User { + name: string; + profilePic: string; +} + +interface HeaderProps { + user: User; + onProfileClick: () => void; + isProfileDetailOpen: boolean; +} + +const ChatRoomHeader: React.FC = ({ + user, + onProfileClick, + isProfileDetailOpen, +}) => { + const nav = useNavigate(); + + return ( +
+
+ + + {/* 프로필 이미지 */} + Profile + + {/* 사용자 이름 */} +
+

+ {user.name} +

+
+
+ + {/* 오른쪽 아이콘 */} +
+ + + +
+
+ ); +}; + +export default ChatRoomHeader; diff --git a/src/components/ChatItem.tsx b/src/components/ChatItem.tsx new file mode 100644 index 00000000..d184961b --- /dev/null +++ b/src/components/ChatItem.tsx @@ -0,0 +1,37 @@ +import { useQuery } from "react-query"; +import { Message, User } from "../pages/ChatRoom"; +import { fetchUsers } from "../api"; +import { useNavigate } from "react-router-dom"; + +const ChatItem = ({ sender, text, chatRoomId, receiver }: Message) => { + const { isLoading, data = [] } = useQuery("messageUser", fetchUsers); + const nav = useNavigate(); + + const findProfilePic = (): User => + data.find((userInfo: User) => userInfo.name === sender); + //filter와 map의 차이 map은 전체 배열을 수정해서 새롭게 만들고 + //filter는 그중에서 찾음 + const getProfilePic = findProfilePic()?.profilePic; + + const onClick = () => { + nav(`/chat-room/${chatRoomId}`, { state: { chatRoomId: chatRoomId } }); + }; + + return ( +
+
+ {!isLoading && ( + + )} +
+
+
+ {sender === "김류원" ? receiver : sender} +
+
{text}
+
+
+ ); +}; + +export default ChatItem; diff --git a/src/components/FriendsList.tsx b/src/components/FriendsList.tsx new file mode 100644 index 00000000..4939c545 --- /dev/null +++ b/src/components/FriendsList.tsx @@ -0,0 +1,37 @@ +import { User } from "../pages/ChatRoom"; + +const FriendsList = ({ + data, + profileOnClick, +}: { + data: User[]; + profileOnClick: (index: number) => void; +}) => { + //data는 query에서 불러옴 + + return ( +
+ {/*최상위 JSX */} + {data.slice(1).map((user, index) => { + return ( +
profileOnClick(index + 1)} + > + +
+
{user.name}
+
{user.description}
+
+
+ ); + })} +
+ ); +}; + +export default FriendsList; diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 00000000..6a5bdac0 --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,38 @@ +import personPlus from "../assets/person_plus.svg"; +import setting from "../assets/setting.svg"; +import video from "../assets/video.svg"; +import search from "../assets/search.svg"; +import { useMatch } from "react-router-dom"; + +const Header = () => { + const match = useMatch("/"); + + return ( +
+
+ {match ? "친구" : "채팅"} +
+ + {/* 오른쪽 아이콘 */} +
+ + {match ? ( + + ) : null} + +
+
+ ); +}; + +export default Header; diff --git a/src/components/MessageInput.tsx b/src/components/MessageInput.tsx new file mode 100644 index 00000000..f9beacc6 --- /dev/null +++ b/src/components/MessageInput.tsx @@ -0,0 +1,80 @@ +import React, { useState, useEffect } from "react"; +import styled, { keyframes } from "styled-components"; +import plus from "../assets/plus.svg"; +import smile from "../assets/smile.svg"; +import thumb_up from "../assets/thumb_up.svg"; +import sendIcon from "../assets/send.svg"; + +interface MessageInputProps { + onSendMessage: (message: string) => void; + isProfileDetailOpen: boolean; +} + +const MessageInput: React.FC = ({ + onSendMessage, + isProfileDetailOpen, +}) => { + const [message, setMessage] = useState(""); + const [isInputClicked, setIsInputClicked] = useState(false); + + const handleSend = (): void => { + if (message.trim() !== "") { + onSendMessage(message); + setMessage(""); + setIsInputClicked(false); + } + }; + + const handleKeyUp = (e: React.KeyboardEvent): void => { + if (e.key === "Enter") { + e.preventDefault(); + handleSend(); + } + }; + + const handleThumbsUp = (): void => { + onSendMessage("👍"); + }; + + return ( +
+ + ) => { + setMessage(e.target.value); + setIsInputClicked(true); + }} + onKeyUp={handleKeyUp} + onFocus={() => setIsInputClicked(true)} + className="flex-1 w-[255px] h-[34px] px-[16px] py-[7px] border focus:outline-none" + style={{ borderRadius: "60px", backgroundColor: "#F0F2F3" }} + placeholder="Aa" + /> + smile Icon + +
+ ); +}; + +export default MessageInput; diff --git a/src/components/MessageList.tsx b/src/components/MessageList.tsx new file mode 100644 index 00000000..0451d985 --- /dev/null +++ b/src/components/MessageList.tsx @@ -0,0 +1,177 @@ +import styled, { keyframes } from "styled-components"; +import React, { useState, useEffect } from "react"; +import { Message } from "../pages/ChatRoom"; + +// interface Message { +// id: string | number; +// sender: string; +// text: string; +// time: string | number | Date; +// } + +interface User { + name: string; +} + +interface MessageListProps { + messages: Message[]; + currentUser: User; + messageData: Message[]; + setMessages: React.Dispatch>; +} + +// 애니메이션 정의 +const fadeIn = keyframes` + from { + opacity: 0; + transform: scale(0.8); + } + to { + opacity: 1; + transform: scale(1); + } +`; + +const MessageList: React.FC = ({ + messages, + currentUser, + messageData, + setMessages, +}) => { + const [currentSender, setCurrentSender] = useState(currentUser.name); + + useEffect(() => { + // 상대방 전환 시 애니메이션이 재실행되도록 설정 + setCurrentSender(currentUser.name); + }, [currentUser.name]); + + const formatTime = (time: string | number | Date): string => { + const date = new Date(time); + return date.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }); + }; + + const formatDate = (time: string | number | Date): string => { + const date = new Date(time); + return `${date.getFullYear()}년 ${ + date.getMonth() + 1 + }월 ${date.getDate()}일`; + }; + + const formatDateTime = (time: string | number | Date): string => { + const date = new Date(time); + return `${date.getFullYear()}년 ${ + date.getMonth() + 1 + }월 ${date.getDate()}일 ${date.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })}`; + }; + + const getProfilePic = (sender: string): string => { + return sender === "CEOS" ? "/Profile_image.svg" : "/Profile_image.svg"; + }; + + let previousDate: string | null = null; + + return ( +
+ {messages.map((message, index) => { + const messageDate = formatDate(message.time); + const showDate = messageDate !== previousDate; + previousDate = messageDate; + + return ( +
+ {showDate && ( + + {formatDateTime(message.time)} + + )} + + {message.sender !== currentUser.name && ( + Profile + )} + + + {message.sender === currentUser.name ? ( + <> + + {formatTime(message.time)} + + + {message.text} + + + ) : ( + <> + + {message.text} + + + {formatTime(message.time)} + + + )} + + +
+ ); + })} +
+ ); +}; + +const DateTimeText = styled.div<{ isCurrentUser: boolean }>` + text-align: center; + font-size: 12px; + color: #737373; + margin: 20px 0; + white-space: nowrap; +`; + +const MessageContainer = styled.div<{ isCurrentUser: boolean }>` + display: flex; + justify-content: ${(props) => + props.isCurrentUser ? "flex-end" : "flex-start"}; + margin-bottom: 16px; + animation: ${fadeIn} 0.3s ease-in-out; +`; + +const MessageContent = styled.div` + display: flex; + align-items: center; + word-break: break-all; +`; + +const MessageText = styled.div<{ isCurrentUser: boolean }>` + background-color: ${(props) => (props.isCurrentUser ? "#0A7CFF" : "#F0F2F3")}; + color: ${(props) => (props.isCurrentUser ? "white" : "#333")}; + padding: 8px 16px; + border-radius: 20px; + font-family: Pretendard; + font-size: 15px; + font-weight: 400; + line-height: 20px; + margin-bottom: 4px; + display: inline-block; +`; + +const TimeText = styled.span<{ isCurrentUser: boolean }>` + font-size: 12px; + color: #b4b8bc; + margin-left: ${(props) => (props.isCurrentUser ? "0" : "4px")}; + margin-right: ${(props) => (props.isCurrentUser ? "4px" : "0")}; + align-self: flex-end; +`; + +export default MessageList; diff --git a/src/components/ProfileInfo.tsx b/src/components/ProfileInfo.tsx new file mode 100644 index 00000000..186c1bbc --- /dev/null +++ b/src/components/ProfileInfo.tsx @@ -0,0 +1,56 @@ +interface User { + name: string; + profilePic: string; +} + +interface ProfileInfoProps { + user: User; + onProfileDetail: () => void; +} + +const ProfileInfo: React.FC = ({ user, onProfileDetail }) => { + if (!user) { + return

사용자 정보가 없습니다.

; + } + + return ( +
+
+ {/* 프로필 이미지 */} + Profile + + {/* 사용자 이름 */} +

+ {user.name} +

+ + {/* 추가 정보 */} +

+ Facebook 친구입니다 +

+

+ 서울특별시 마포 거주 +

+ + {/* 프로필 보기 버튼 */} + +
+
+ ); +}; + +export default ProfileInfo; diff --git a/src/index.css b/src/index.css index ec2585e8..5b15d903 100644 --- a/src/index.css +++ b/src/index.css @@ -1,13 +1,6 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; + font-family: "Pretendard"; } diff --git a/src/index.tsx b/src/index.tsx index d10be77d..989e4a45 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,13 +1,22 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import './index.css'; -import App from './App'; +import { RecoilRoot } from "recoil"; +import ReactDOM from "react-dom/client"; +import "./index.css"; +import App from "./App"; +import { BrowserRouter } from "react-router-dom"; + +import { QueryClient, QueryClientProvider } from "react-query"; + +const queryClient = new QueryClient(); const root = ReactDOM.createRoot( - document.getElementById('root') as HTMLElement + document.getElementById("root") as HTMLElement ); root.render( - - - + + + + + + + ); diff --git a/src/pages/ChatList.tsx b/src/pages/ChatList.tsx new file mode 100644 index 00000000..7f799bb3 --- /dev/null +++ b/src/pages/ChatList.tsx @@ -0,0 +1,78 @@ +import { useQuery } from "react-query"; +import ChatItem from "../components/ChatItem"; +import Header from "../components/Header"; +import { fetchMessages, fetchUsers } from "../api"; +import { Message, User } from "./ChatRoom"; +import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; + +const ChatList = () => { + const { isLoading, data = [] } = useQuery( + "messages", + fetchMessages + ); + + const [messageList, setMessageList] = useState([]); + + useEffect(() => { + let parsedMessages: Message[] = []; + + // 전체 `localStorage`에서 마지막 메세지 불러오기 + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key) { + // 각 키의 메시지 데이터를 JSON.parse로 배열로 변환 + const storedMessages = JSON.parse(localStorage.getItem(key) || "[]"); + + // 배열인지 확인 후에 `reduce` 메서드 사용 + if (Array.isArray(storedMessages)) { + const latestMessage = storedMessages.reduce( + (latest: Message | null, item: Message) => { + return !latest || new Date(item.time) > new Date(latest.time) + ? item + : latest; + }, + null + ); + + if (latestMessage) { + parsedMessages = [...parsedMessages, latestMessage]; + } + } + } + } + + const JsonMessages = data.reduceRight((p: Message[], n: Message) => { + if (!p.find((item) => item.chatRoomId === n.chatRoomId)) { + return [...p, n]; + } + return p; + }, []); + + const filteredMessages = parsedMessages + .concat(JsonMessages) + .reduce((p: Message[], n: Message) => { + if ( + !p.find((item) => item.chatRoomId === n.chatRoomId) && + !p.find((item) => item === n) + ) { + return [...p, n]; + } + return p; + }, []); + filteredMessages.sort( + (a, b): number => new Date(b.time).getTime() - new Date(a.time).getTime() + ); + setMessageList(filteredMessages); + }, [data]); + + // console.log(data); + return ( +
+
+ {messageList && messageList.map((item) => )} +
+ ); +}; + +export default ChatList; diff --git a/src/pages/ChatRoom.tsx b/src/pages/ChatRoom.tsx new file mode 100644 index 00000000..d5f2ee10 --- /dev/null +++ b/src/pages/ChatRoom.tsx @@ -0,0 +1,168 @@ +import React, { useState, useEffect, useRef } from "react"; +import MessageList from "../components/MessageList"; +import MessageInput from "../components/MessageInput"; +import ProfileInfo from "../components/ProfileInfo"; +import CHeader from "../components/CHeader"; +import ProfileDetail from "../pages/ProfileDetail"; +import { useQuery } from "react-query"; +import { fetchMessages, fetchUsers } from "../api"; +import { useLocation, useParams } from "react-router-dom"; + +export interface User { + id: number; + name: string; + profilePic: string; + facebook: string; + instagram: string; + description?: string; +} + +export interface Message { + id: number; + text: string; + sender: string; + receiver: string; + time: string; + chatRoomId: string; +} + +const ChatRoom: React.FC = () => { + const { isLoading: messageLoading, data: messageData = [] } = useQuery< + Message[] + >("chat-message", fetchMessages); + const { isLoading: userLoading, data: userData = [] } = useQuery( + "chat-user", + fetchUsers + ); + const location = useLocation(); + const params = useParams(); + const chatRoomId = location.state?.chatRoomId || params.sender; + + const [messages, setMessages] = useState([]); + const [currentUser, setCurrentUser] = useState(); + const [otherUser, setOtherUser] = useState(); + + const ryuwon = userData.find((item) => item.name === "김류원"); + const other = userData.find((item) => item.name === chatRoomId); + + const chatKey = `conversationMessages_${chatRoomId}`; + + useEffect(() => { + if (ryuwon) { + setCurrentUser(ryuwon); + } + }, [userData, ryuwon]); + + useEffect(() => { + if (other) { + setOtherUser(other); + } + }, [params.sender, chatRoomId, other]); + + useEffect(() => { + if (messages.length === 0) { + const storedMessages = localStorage.getItem(chatKey); + if (storedMessages) { + const parsedMessages = JSON.parse(storedMessages).filter( + (item: Message) => + item.sender === chatRoomId || item.receiver === chatRoomId + ); + if (parsedMessages.length > 0) { + setMessages(parsedMessages); + return; + } + } + + const filteredMessages = messageData.filter( + (item) => + (item.sender === chatRoomId && item.receiver === "김류원") || + (item.sender === "김류원" && item.receiver === chatRoomId) + ); + setMessages(filteredMessages); + } + }, [chatRoomId, messageData, messages, chatKey, params.sender]); + + const [isProfileDetailOpen, setIsProfileDetailOpen] = useState(false); + const messagesEndRef = useRef(null); + + useEffect(() => { + if (messagesEndRef.current) { + messagesEndRef.current.scrollIntoView({ behavior: "smooth" }); + } + }, [messages]); + + const toggleUser = () => { + setCurrentUser(currentUser?.name === "김류원" ? other : ryuwon); + setOtherUser(currentUser?.name === "김류원" ? ryuwon : other); + }; + + const handleProfileDetail = () => { + setIsProfileDetailOpen(true); + }; + + const closeProfileDetail = () => { + setIsProfileDetailOpen(false); + }; + + const handleSendMessage = (newMessage: string) => { + const currentTime = new Date().toISOString(); + const newMessageData: Message = { + id: messages.length + 1, + text: newMessage, + sender: "김류원", + receiver: currentUser + ? currentUser.name === "김류원" + ? otherUser?.name || "김류원" + : currentUser.name + : "김류원", + time: currentTime, + chatRoomId: `${chatRoomId}`, + }; + + const updatedMessages = [...messages, newMessageData]; + setMessages(updatedMessages); + localStorage.setItem(chatKey, JSON.stringify(updatedMessages)); + }; + + const loading = userLoading || messageLoading || !currentUser || !otherUser; + + return loading ? ( +
로딩중
+ ) : ( +
+ + +
+ + +
+
+ + + {isProfileDetailOpen && ( +
+ +
+ )} +
+ ); +}; + +export default ChatRoom; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx new file mode 100644 index 00000000..60ee4d56 --- /dev/null +++ b/src/pages/Home.tsx @@ -0,0 +1,81 @@ +import Header from "../components/Header"; +import { useQuery } from "react-query"; +import { fetchUsers } from "../api"; +import { User } from "./ChatRoom"; +import FriendsList from "../components/FriendsList"; +import { useState } from "react"; +import ProfileDetail from "./ProfileDetail"; + +const Home = () => { + const { isLoading, data = [] } = useQuery("users", fetchUsers); + const [isProfileDetailOpen, setIsProfileDetailOpen] = useState(false); + const [profileUser, setProfileUser] = useState(data[0]); + + const profileOnClick = (index: number) => { + setIsProfileDetailOpen(true); + setProfileUser(data[index]); + }; + + const closeProfileDetail = () => { + setIsProfileDetailOpen(false); + }; + return ( +
+
+ + {isLoading ? ( +
로딩중
+ ) : ( +
+ {/* 사용자 프로필 */} +
profileOnClick(0)} + > + +
+

{data[0].name}

+
+
+ {/* 활동 상태 */} +
+

활동 상태

+
+ {data.slice(1).map((item, i) => ( +
+
+ + +
+

{item.name}

+
+ ))} +
+
+ {/* 친구 목록 */} +
+

+ 친구 {data.length} +

+ +
+
+ )} + {/* 프로필 상세 보기 */} + {isProfileDetailOpen && ( +
+ +
+ )} +
+ ); +}; + +export default Home; diff --git a/src/pages/NotFound.tsx b/src/pages/NotFound.tsx new file mode 100644 index 00000000..d00c42c1 --- /dev/null +++ b/src/pages/NotFound.tsx @@ -0,0 +1,5 @@ +const NotFound = () => { + return
잘못된 경로입니다!
; +}; + +export default NotFound; diff --git a/src/pages/ProfileDetail.tsx b/src/pages/ProfileDetail.tsx new file mode 100644 index 00000000..011d87d2 --- /dev/null +++ b/src/pages/ProfileDetail.tsx @@ -0,0 +1,89 @@ +import cancel from "../assets/cancel.svg"; +import settings from "../assets/setting.svg"; +import facebook from "../assets/profile_facebook.svg"; +import instagram from "../assets/profile_insta.svg"; +import plus from "../assets/profile_plus.svg"; +import edit from "../assets/edit.svg"; +import { useMatch, useNavigate } from "react-router-dom"; + +interface User { + name: string; + profilePic: string; + facebook: string; + instagram: string; +} + +interface ProfileDetailProps { + user: User; + onClose: () => void; +} + +const ProfileDetail: React.FC = ({ user, onClose }) => { + const match = useMatch("/"); + const nav = useNavigate(); + + return ( +
+
+ + +
+ +
+ +
+ +

{user.name}

+ +
+ + + + +
+
+ ); +}; + +export default ProfileDetail; diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 00000000..ecf62b50 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: {}, + }, + plugins: [], +};